/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/bzr/inventory.py

  • Committer: Martin
  • Date: 2018-11-17 17:37:42 UTC
  • mto: This revision was merged to the branch mainline in revision 7188.
  • Revision ID: gzlist@googlemail.com-20181117173742-nojh69vdpnofhtw7
Fix E27* lint errors

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005-2011 Canonical Ltd
 
2
#
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
#
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
#
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
16
 
 
17
# FIXME: This refactoring of the workingtree code doesn't seem to keep
 
18
# the WorkingTree's copy of the inventory in sync with the branch.  The
 
19
# branch modifies its working inventory when it does a commit to make
 
20
# missing files permanently removed.
 
21
 
 
22
# TODO: Maybe also keep the full path of the entry, and the children?
 
23
# But those depend on its position within a particular inventory, and
 
24
# it would be nice not to need to hold the backpointer here.
 
25
 
 
26
from __future__ import absolute_import
 
27
 
 
28
# This should really be an id randomly assigned when the tree is
 
29
# created, but it's not for now.
 
30
ROOT_ID = b"TREE_ROOT"
 
31
 
 
32
from ..lazy_import import lazy_import
 
33
lazy_import(globals(), """
 
34
import collections
 
35
 
 
36
from breezy import (
 
37
    generate_ids,
 
38
    osutils,
 
39
    )
 
40
from breezy.bzr import (
 
41
    chk_map,
 
42
    )
 
43
""")
 
44
 
 
45
from .. import (
 
46
    errors,
 
47
    lazy_regex,
 
48
    trace,
 
49
    )
 
50
from ..sixish import (
 
51
    bytesintern,
 
52
    text_type,
 
53
    viewitems,
 
54
    viewvalues,
 
55
    )
 
56
from ..static_tuple import StaticTuple
 
57
 
 
58
 
 
59
class InventoryEntry(object):
 
60
    """Description of a versioned file.
 
61
 
 
62
    An InventoryEntry has the following fields, which are also
 
63
    present in the XML inventory-entry element:
 
64
 
 
65
    file_id
 
66
 
 
67
    name
 
68
        (within the parent directory)
 
69
 
 
70
    parent_id
 
71
        file_id of the parent directory, or ROOT_ID
 
72
 
 
73
    revision
 
74
        the revision_id in which this variation of this file was
 
75
        introduced.
 
76
 
 
77
    executable
 
78
        Indicates that this file should be executable on systems
 
79
        that support it.
 
80
 
 
81
    text_sha1
 
82
        sha-1 of the text of the file
 
83
 
 
84
    text_size
 
85
        size in bytes of the text of the file
 
86
 
 
87
    (reading a version 4 tree created a text_id field.)
 
88
 
 
89
    >>> i = Inventory()
 
90
    >>> i.path2id('')
 
91
    'TREE_ROOT'
 
92
    >>> i.add(InventoryDirectory(b'123', 'src', ROOT_ID))
 
93
    InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None)
 
94
    >>> i.add(InventoryFile(b'2323', 'hello.c', parent_id='123'))
 
95
    InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None, revision=None)
 
96
    >>> shouldbe = {0: '', 1: 'src', 2: 'src/hello.c'}
 
97
    >>> for ix, j in enumerate(i.iter_entries()):
 
98
    ...   print (j[0] == shouldbe[ix], j[1])
 
99
    ...
 
100
    (True, InventoryDirectory('TREE_ROOT', u'', parent_id=None, revision=None))
 
101
    (True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None))
 
102
    (True, InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None, revision=None))
 
103
    >>> i.add(InventoryFile('2324', 'bye.c', '123'))
 
104
    InventoryFile('2324', 'bye.c', parent_id='123', sha1=None, len=None, revision=None)
 
105
    >>> i.add(InventoryDirectory('2325', 'wibble', '123'))
 
106
    InventoryDirectory('2325', 'wibble', parent_id='123', revision=None)
 
107
    >>> i.path2id('src/wibble')
 
108
    '2325'
 
109
    >>> i.add(InventoryFile('2326', 'wibble.c', '2325'))
 
110
    InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None, revision=None)
 
111
    >>> i.get_entry('2326')
 
112
    InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None, revision=None)
 
113
    >>> for path, entry in i.iter_entries():
 
114
    ...     print path
 
115
    ...
 
116
    <BLANKLINE>
 
117
    src
 
118
    src/bye.c
 
119
    src/hello.c
 
120
    src/wibble
 
121
    src/wibble/wibble.c
 
122
    >>> i.id2path(b'2326')
 
123
    'src/wibble/wibble.c'
 
124
    """
 
125
 
 
126
    # Constants returned by describe_change()
 
127
    #
 
128
    # TODO: These should probably move to some kind of FileChangeDescription
 
129
    # class; that's like what's inside a TreeDelta but we want to be able to
 
130
    # generate them just for one file at a time.
 
131
    RENAMED = 'renamed'
 
132
    MODIFIED_AND_RENAMED = 'modified and renamed'
 
133
 
 
134
    __slots__ = ['file_id', 'revision', 'parent_id', 'name']
 
135
 
 
136
    # Attributes that all InventoryEntry instances are expected to have, but
 
137
    # that don't vary for all kinds of entry.  (e.g. symlink_target is only
 
138
    # relevant to InventoryLink, so there's no reason to make every
 
139
    # InventoryFile instance allocate space to hold a value for it.)
 
140
    # Attributes that only vary for files: executable, text_sha1, text_size,
 
141
    # text_id
 
142
    executable = False
 
143
    text_sha1 = None
 
144
    text_size = None
 
145
    text_id = None
 
146
    # Attributes that only vary for symlinks: symlink_target
 
147
    symlink_target = None
 
148
    # Attributes that only vary for tree-references: reference_revision
 
149
    reference_revision = None
 
150
 
 
151
    def detect_changes(self, old_entry):
 
152
        """Return a (text_modified, meta_modified) from this to old_entry.
 
153
 
 
154
        _read_tree_state must have been called on self and old_entry prior to
 
155
        calling detect_changes.
 
156
        """
 
157
        return False, False
 
158
 
 
159
    def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
 
160
              output_to, reverse=False):
 
161
        """Perform a diff between two entries of the same kind."""
 
162
 
 
163
    def parent_candidates(self, previous_inventories):
 
164
        """Find possible per-file graph parents.
 
165
 
 
166
        This is currently defined by:
 
167
         - Select the last changed revision in the parent inventory.
 
168
         - Do deal with a short lived bug in bzr 0.8's development two entries
 
169
           that have the same last changed but different 'x' bit settings are
 
170
           changed in-place.
 
171
        """
 
172
        # revision:ie mapping for each ie found in previous_inventories.
 
173
        candidates = {}
 
174
        # identify candidate head revision ids.
 
175
        for inv in previous_inventories:
 
176
            try:
 
177
                ie = inv.get_entry(self.file_id)
 
178
            except errors.NoSuchId:
 
179
                pass
 
180
            else:
 
181
                if ie.revision in candidates:
 
182
                    # same revision value in two different inventories:
 
183
                    # correct possible inconsistencies:
 
184
                    #     * there was a bug in revision updates with 'x' bit
 
185
                    #       support.
 
186
                    try:
 
187
                        if candidates[ie.revision].executable != ie.executable:
 
188
                            candidates[ie.revision].executable = False
 
189
                            ie.executable = False
 
190
                    except AttributeError:
 
191
                        pass
 
192
                else:
 
193
                    # add this revision as a candidate.
 
194
                    candidates[ie.revision] = ie
 
195
        return candidates
 
196
 
 
197
    def has_text(self):
 
198
        """Return true if the object this entry represents has textual data.
 
199
 
 
200
        Note that textual data includes binary content.
 
201
 
 
202
        Also note that all entries get weave files created for them.
 
203
        This attribute is primarily used when upgrading from old trees that
 
204
        did not have the weave index for all inventory entries.
 
205
        """
 
206
        return False
 
207
 
 
208
    def __init__(self, file_id, name, parent_id):
 
209
        """Create an InventoryEntry
 
210
 
 
211
        The filename must be a single component, relative to the
 
212
        parent directory; it cannot be a whole path or relative name.
 
213
 
 
214
        >>> e = InventoryFile(b'123', 'hello.c', ROOT_ID)
 
215
        >>> e.name
 
216
        'hello.c'
 
217
        >>> e.file_id
 
218
        '123'
 
219
        >>> e = InventoryFile(b'123', 'src/hello.c', ROOT_ID)
 
220
        Traceback (most recent call last):
 
221
        InvalidEntryName: Invalid entry name: src/hello.c
 
222
        """
 
223
        if u'/' in name:
 
224
            raise errors.InvalidEntryName(name=name)
 
225
        if not isinstance(file_id, bytes):
 
226
            raise TypeError(file_id)
 
227
        self.file_id = file_id
 
228
        self.revision = None
 
229
        self.name = name
 
230
        self.parent_id = parent_id
 
231
 
 
232
    def kind_character(self):
 
233
        """Return a short kind indicator useful for appending to names."""
 
234
        raise errors.BzrError('unknown kind %r' % self.kind)
 
235
 
 
236
    known_kinds = ('file', 'directory', 'symlink')
 
237
 
 
238
    @staticmethod
 
239
    def versionable_kind(kind):
 
240
        return (kind in ('file', 'directory', 'symlink', 'tree-reference'))
 
241
 
 
242
    def check(self, checker, rev_id, inv):
 
243
        """Check this inventory entry is intact.
 
244
 
 
245
        This is a template method, override _check for kind specific
 
246
        tests.
 
247
 
 
248
        :param checker: Check object providing context for the checks;
 
249
             can be used to find out what parts of the repository have already
 
250
             been checked.
 
251
        :param rev_id: Revision id from which this InventoryEntry was loaded.
 
252
             Not necessarily the last-changed revision for this file.
 
253
        :param inv: Inventory from which the entry was loaded.
 
254
        """
 
255
        if self.parent_id is not None:
 
256
            if not inv.has_id(self.parent_id):
 
257
                raise errors.BzrCheckError(
 
258
                    'missing parent {%s} in inventory for revision {%s}' % (
 
259
                        self.parent_id, rev_id))
 
260
        checker._add_entry_to_text_key_references(inv, self)
 
261
        self._check(checker, rev_id)
 
262
 
 
263
    def _check(self, checker, rev_id):
 
264
        """Check this inventory entry for kind specific errors."""
 
265
        checker._report_items.append(
 
266
            'unknown entry kind %r in revision {%s}' % (self.kind, rev_id))
 
267
 
 
268
    def copy(self):
 
269
        """Clone this inventory entry."""
 
270
        raise NotImplementedError
 
271
 
 
272
    @staticmethod
 
273
    def describe_change(old_entry, new_entry):
 
274
        """Describe the change between old_entry and this.
 
275
 
 
276
        This smells of being an InterInventoryEntry situation, but as its
 
277
        the first one, we're making it a static method for now.
 
278
 
 
279
        An entry with a different parent, or different name is considered
 
280
        to be renamed. Reparenting is an internal detail.
 
281
        Note that renaming the parent does not trigger a rename for the
 
282
        child entry itself.
 
283
        """
 
284
        # TODO: Perhaps return an object rather than just a string
 
285
        if old_entry is new_entry:
 
286
            # also the case of both being None
 
287
            return 'unchanged'
 
288
        elif old_entry is None:
 
289
            return 'added'
 
290
        elif new_entry is None:
 
291
            return 'removed'
 
292
        if old_entry.kind != new_entry.kind:
 
293
            return 'modified'
 
294
        text_modified, meta_modified = new_entry.detect_changes(old_entry)
 
295
        if text_modified or meta_modified:
 
296
            modified = True
 
297
        else:
 
298
            modified = False
 
299
        # TODO 20060511 (mbp, rbc) factor out 'detect_rename' here.
 
300
        if old_entry.parent_id != new_entry.parent_id:
 
301
            renamed = True
 
302
        elif old_entry.name != new_entry.name:
 
303
            renamed = True
 
304
        else:
 
305
            renamed = False
 
306
        if renamed and not modified:
 
307
            return InventoryEntry.RENAMED
 
308
        if modified and not renamed:
 
309
            return 'modified'
 
310
        if modified and renamed:
 
311
            return InventoryEntry.MODIFIED_AND_RENAMED
 
312
        return 'unchanged'
 
313
 
 
314
    def __repr__(self):
 
315
        return ("%s(%r, %r, parent_id=%r, revision=%r)"
 
316
                % (self.__class__.__name__,
 
317
                   self.file_id,
 
318
                   self.name,
 
319
                   self.parent_id,
 
320
                   self.revision))
 
321
 
 
322
    def __eq__(self, other):
 
323
        if other is self:
 
324
            # For the case when objects are cached
 
325
            return True
 
326
        if not isinstance(other, InventoryEntry):
 
327
            return NotImplemented
 
328
 
 
329
        return ((self.file_id == other.file_id) and
 
330
                (self.name == other.name) and
 
331
                (other.symlink_target == self.symlink_target) and
 
332
                (self.text_sha1 == other.text_sha1) and
 
333
                (self.text_size == other.text_size) and
 
334
                (self.text_id == other.text_id) and
 
335
                (self.parent_id == other.parent_id) and
 
336
                (self.kind == other.kind) and
 
337
                (self.revision == other.revision) and
 
338
                (self.executable == other.executable) and
 
339
                (self.reference_revision == other.reference_revision)
 
340
                )
 
341
 
 
342
    def __ne__(self, other):
 
343
        return not (self == other)
 
344
 
 
345
    def __hash__(self):
 
346
        raise ValueError('not hashable')
 
347
 
 
348
    def _unchanged(self, previous_ie):
 
349
        """Has this entry changed relative to previous_ie.
 
350
 
 
351
        This method should be overridden in child classes.
 
352
        """
 
353
        compatible = True
 
354
        # different inv parent
 
355
        if previous_ie.parent_id != self.parent_id:
 
356
            compatible = False
 
357
        # renamed
 
358
        elif previous_ie.name != self.name:
 
359
            compatible = False
 
360
        elif previous_ie.kind != self.kind:
 
361
            compatible = False
 
362
        return compatible
 
363
 
 
364
    def _read_tree_state(self, path, work_tree):
 
365
        """Populate fields in the inventory entry from the given tree.
 
366
 
 
367
        Note that this should be modified to be a noop on virtual trees
 
368
        as all entries created there are prepopulated.
 
369
        """
 
370
        # TODO: Rather than running this manually, we should check the
 
371
        # working sha1 and other expensive properties when they're
 
372
        # first requested, or preload them if they're already known
 
373
        pass            # nothing to do by default
 
374
 
 
375
    def _forget_tree_state(self):
 
376
        pass
 
377
 
 
378
 
 
379
class InventoryDirectory(InventoryEntry):
 
380
    """A directory in an inventory."""
 
381
 
 
382
    __slots__ = ['children']
 
383
 
 
384
    kind = 'directory'
 
385
 
 
386
    def _check(self, checker, rev_id):
 
387
        """See InventoryEntry._check"""
 
388
        # In non rich root repositories we do not expect a file graph for the
 
389
        # root.
 
390
        if self.name == '' and not checker.rich_roots:
 
391
            return
 
392
        # Directories are stored as an empty file, but the file should exist
 
393
        # to provide a per-fileid log. The hash of every directory content is
 
394
        # "da..." below (the sha1sum of '').
 
395
        checker.add_pending_item(rev_id,
 
396
                                 ('texts', self.file_id, self.revision), b'text',
 
397
                                 b'da39a3ee5e6b4b0d3255bfef95601890afd80709')
 
398
 
 
399
    def copy(self):
 
400
        other = InventoryDirectory(self.file_id, self.name, self.parent_id)
 
401
        other.revision = self.revision
 
402
        # note that children are *not* copied; they're pulled across when
 
403
        # others are added
 
404
        return other
 
405
 
 
406
    def __init__(self, file_id, name, parent_id):
 
407
        super(InventoryDirectory, self).__init__(file_id, name, parent_id)
 
408
        self.children = {}
 
409
 
 
410
    def sorted_children(self):
 
411
        return sorted(viewitems(self.children))
 
412
 
 
413
    def kind_character(self):
 
414
        """See InventoryEntry.kind_character."""
 
415
        return '/'
 
416
 
 
417
 
 
418
class InventoryFile(InventoryEntry):
 
419
    """A file in an inventory."""
 
420
 
 
421
    __slots__ = ['text_sha1', 'text_size', 'text_id', 'executable']
 
422
 
 
423
    kind = 'file'
 
424
 
 
425
    def __init__(self, file_id, name, parent_id):
 
426
        super(InventoryFile, self).__init__(file_id, name, parent_id)
 
427
        self.text_sha1 = None
 
428
        self.text_size = None
 
429
        self.text_id = None
 
430
        self.executable = False
 
431
 
 
432
    def _check(self, checker, tree_revision_id):
 
433
        """See InventoryEntry._check"""
 
434
        # TODO: check size too.
 
435
        checker.add_pending_item(tree_revision_id,
 
436
                                 ('texts', self.file_id, self.revision), b'text',
 
437
                                 self.text_sha1)
 
438
        if self.text_size is None:
 
439
            checker._report_items.append(
 
440
                'fileid {%s} in {%s} has None for text_size' % (self.file_id,
 
441
                                                                tree_revision_id))
 
442
 
 
443
    def copy(self):
 
444
        other = InventoryFile(self.file_id, self.name, self.parent_id)
 
445
        other.executable = self.executable
 
446
        other.text_id = self.text_id
 
447
        other.text_sha1 = self.text_sha1
 
448
        other.text_size = self.text_size
 
449
        other.revision = self.revision
 
450
        return other
 
451
 
 
452
    def detect_changes(self, old_entry):
 
453
        """See InventoryEntry.detect_changes."""
 
454
        text_modified = (self.text_sha1 != old_entry.text_sha1)
 
455
        meta_modified = (self.executable != old_entry.executable)
 
456
        return text_modified, meta_modified
 
457
 
 
458
    def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
 
459
              output_to, reverse=False):
 
460
        """See InventoryEntry._diff."""
 
461
        from breezy.diff import DiffText
 
462
        from_file_id = self.file_id
 
463
        if to_entry:
 
464
            to_file_id = to_entry.file_id
 
465
            to_path = to_tree.id2path(to_file_id)
 
466
        else:
 
467
            to_file_id = None
 
468
            to_path = None
 
469
        if from_file_id is not None:
 
470
            from_path = tree.id2path(from_file_id)
 
471
        else:
 
472
            from_path = None
 
473
        if reverse:
 
474
            to_file_id, from_file_id = from_file_id, to_file_id
 
475
            tree, to_tree = to_tree, tree
 
476
            from_label, to_label = to_label, from_label
 
477
        differ = DiffText(tree, to_tree, output_to, 'utf-8', '', '',
 
478
                          text_diff)
 
479
        return differ.diff_text(from_path, to_path, from_label, to_label,
 
480
                                from_file_id, to_file_id)
 
481
 
 
482
    def has_text(self):
 
483
        """See InventoryEntry.has_text."""
 
484
        return True
 
485
 
 
486
    def kind_character(self):
 
487
        """See InventoryEntry.kind_character."""
 
488
        return ''
 
489
 
 
490
    def _read_tree_state(self, path, work_tree):
 
491
        """See InventoryEntry._read_tree_state."""
 
492
        self.text_sha1 = work_tree.get_file_sha1(path, self.file_id)
 
493
        # FIXME: 20050930 probe for the text size when getting sha1
 
494
        # in _read_tree_state
 
495
        self.executable = work_tree.is_executable(path, self.file_id)
 
496
 
 
497
    def __repr__(self):
 
498
        return ("%s(%r, %r, parent_id=%r, sha1=%r, len=%s, revision=%s)"
 
499
                % (self.__class__.__name__,
 
500
                   self.file_id,
 
501
                   self.name,
 
502
                   self.parent_id,
 
503
                   self.text_sha1,
 
504
                   self.text_size,
 
505
                   self.revision))
 
506
 
 
507
    def _forget_tree_state(self):
 
508
        self.text_sha1 = None
 
509
 
 
510
    def _unchanged(self, previous_ie):
 
511
        """See InventoryEntry._unchanged."""
 
512
        compatible = super(InventoryFile, self)._unchanged(previous_ie)
 
513
        if self.text_sha1 != previous_ie.text_sha1:
 
514
            compatible = False
 
515
        else:
 
516
            # FIXME: 20050930 probe for the text size when getting sha1
 
517
            # in _read_tree_state
 
518
            self.text_size = previous_ie.text_size
 
519
        if self.executable != previous_ie.executable:
 
520
            compatible = False
 
521
        return compatible
 
522
 
 
523
 
 
524
class InventoryLink(InventoryEntry):
 
525
    """A file in an inventory."""
 
526
 
 
527
    __slots__ = ['symlink_target']
 
528
 
 
529
    kind = 'symlink'
 
530
 
 
531
    def __init__(self, file_id, name, parent_id):
 
532
        super(InventoryLink, self).__init__(file_id, name, parent_id)
 
533
        self.symlink_target = None
 
534
 
 
535
    def _check(self, checker, tree_revision_id):
 
536
        """See InventoryEntry._check"""
 
537
        if self.symlink_target is None:
 
538
            checker._report_items.append(
 
539
                'symlink {%s} has no target in revision {%s}'
 
540
                % (self.file_id, tree_revision_id))
 
541
        # Symlinks are stored as ''
 
542
        checker.add_pending_item(tree_revision_id,
 
543
                                 ('texts', self.file_id, self.revision), b'text',
 
544
                                 b'da39a3ee5e6b4b0d3255bfef95601890afd80709')
 
545
 
 
546
    def copy(self):
 
547
        other = InventoryLink(self.file_id, self.name, self.parent_id)
 
548
        other.symlink_target = self.symlink_target
 
549
        other.revision = self.revision
 
550
        return other
 
551
 
 
552
    def detect_changes(self, old_entry):
 
553
        """See InventoryEntry.detect_changes."""
 
554
        # FIXME: which _modified field should we use ? RBC 20051003
 
555
        text_modified = (self.symlink_target != old_entry.symlink_target)
 
556
        if text_modified:
 
557
            trace.mutter("    symlink target changed")
 
558
        meta_modified = False
 
559
        return text_modified, meta_modified
 
560
 
 
561
    def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
 
562
              output_to, reverse=False):
 
563
        """See InventoryEntry._diff."""
 
564
        from breezy.diff import DiffSymlink
 
565
        old_target = self.symlink_target
 
566
        if to_entry is not None:
 
567
            new_target = to_entry.symlink_target
 
568
        else:
 
569
            new_target = None
 
570
        if not reverse:
 
571
            old_tree = tree
 
572
            new_tree = to_tree
 
573
        else:
 
574
            old_tree = to_tree
 
575
            new_tree = tree
 
576
            new_target, old_target = old_target, new_target
 
577
        differ = DiffSymlink(old_tree, new_tree, output_to)
 
578
        return differ.diff_symlink(old_target, new_target)
 
579
 
 
580
    def kind_character(self):
 
581
        """See InventoryEntry.kind_character."""
 
582
        return ''
 
583
 
 
584
    def _read_tree_state(self, path, work_tree):
 
585
        """See InventoryEntry._read_tree_state."""
 
586
        self.symlink_target = work_tree.get_symlink_target(
 
587
            work_tree.id2path(self.file_id), self.file_id)
 
588
 
 
589
    def _forget_tree_state(self):
 
590
        self.symlink_target = None
 
591
 
 
592
    def _unchanged(self, previous_ie):
 
593
        """See InventoryEntry._unchanged."""
 
594
        compatible = super(InventoryLink, self)._unchanged(previous_ie)
 
595
        if self.symlink_target != previous_ie.symlink_target:
 
596
            compatible = False
 
597
        return compatible
 
598
 
 
599
 
 
600
class TreeReference(InventoryEntry):
 
601
 
 
602
    __slots__ = ['reference_revision']
 
603
 
 
604
    kind = 'tree-reference'
 
605
 
 
606
    def __init__(self, file_id, name, parent_id, revision=None,
 
607
                 reference_revision=None):
 
608
        InventoryEntry.__init__(self, file_id, name, parent_id)
 
609
        self.revision = revision
 
610
        self.reference_revision = reference_revision
 
611
 
 
612
    def copy(self):
 
613
        return TreeReference(self.file_id, self.name, self.parent_id,
 
614
                             self.revision, self.reference_revision)
 
615
 
 
616
    def _read_tree_state(self, path, work_tree):
 
617
        """Populate fields in the inventory entry from the given tree.
 
618
        """
 
619
        self.reference_revision = work_tree.get_reference_revision(
 
620
            path, self.file_id)
 
621
 
 
622
    def _forget_tree_state(self):
 
623
        self.reference_revision = None
 
624
 
 
625
    def _unchanged(self, previous_ie):
 
626
        """See InventoryEntry._unchanged."""
 
627
        compatible = super(TreeReference, self)._unchanged(previous_ie)
 
628
        if self.reference_revision != previous_ie.reference_revision:
 
629
            compatible = False
 
630
        return compatible
 
631
 
 
632
    def kind_character(self):
 
633
        """See InventoryEntry.kind_character."""
 
634
        return '+'
 
635
 
 
636
 
 
637
class CommonInventory(object):
 
638
    """Basic inventory logic, defined in terms of primitives like has_id.
 
639
 
 
640
    An inventory is the metadata about the contents of a tree.
 
641
 
 
642
    This is broadly a map from file_id to entries such as directories, files,
 
643
    symlinks and tree references. Each entry maintains its own metadata like
 
644
    SHA1 and length for files, or children for a directory.
 
645
 
 
646
    Entries can be looked up either by path or by file_id.
 
647
 
 
648
    InventoryEntry objects must not be modified after they are
 
649
    inserted, other than through the Inventory API.
 
650
    """
 
651
 
 
652
    def has_filename(self, filename):
 
653
        return bool(self.path2id(filename))
 
654
 
 
655
    def id2path(self, file_id):
 
656
        """Return as a string the path to file_id.
 
657
 
 
658
        >>> i = Inventory()
 
659
        >>> e = i.add(InventoryDirectory(b'src-id', 'src', ROOT_ID))
 
660
        >>> e = i.add(InventoryFile(b'foo-id', 'foo.c', parent_id='src-id'))
 
661
        >>> print i.id2path(b'foo-id')
 
662
        src/foo.c
 
663
 
 
664
        :raises NoSuchId: If file_id is not present in the inventory.
 
665
        """
 
666
        # get all names, skipping root
 
667
        return '/'.join(reversed(
 
668
            [parent.name for parent in
 
669
             self._iter_file_id_parents(file_id)][:-1]))
 
670
 
 
671
    def iter_entries(self, from_dir=None, recursive=True):
 
672
        """Return (path, entry) pairs, in order by name.
 
673
 
 
674
        :param from_dir: if None, start from the root,
 
675
          otherwise start from this directory (either file-id or entry)
 
676
        :param recursive: recurse into directories or not
 
677
        """
 
678
        if from_dir is None:
 
679
            if self.root is None:
 
680
                return
 
681
            from_dir = self.root
 
682
            yield '', self.root
 
683
        elif isinstance(from_dir, bytes):
 
684
            from_dir = self.get_entry(from_dir)
 
685
 
 
686
        # unrolling the recursive called changed the time from
 
687
        # 440ms/663ms (inline/total) to 116ms/116ms
 
688
        children = sorted(viewitems(from_dir.children))
 
689
        if not recursive:
 
690
            for name, ie in children:
 
691
                yield name, ie
 
692
            return
 
693
        children = collections.deque(children)
 
694
        stack = [(u'', children)]
 
695
        while stack:
 
696
            from_dir_relpath, children = stack[-1]
 
697
 
 
698
            while children:
 
699
                name, ie = children.popleft()
 
700
 
 
701
                # we know that from_dir_relpath never ends in a slash
 
702
                # and 'f' doesn't begin with one, we can do a string op, rather
 
703
                # than the checks of pathjoin(), though this means that all paths
 
704
                # start with a slash
 
705
                path = from_dir_relpath + '/' + name
 
706
 
 
707
                yield path[1:], ie
 
708
 
 
709
                if ie.kind != 'directory':
 
710
                    continue
 
711
 
 
712
                # But do this child first
 
713
                new_children = sorted(viewitems(ie.children))
 
714
                new_children = collections.deque(new_children)
 
715
                stack.append((path, new_children))
 
716
                # Break out of inner loop, so that we start outer loop with child
 
717
                break
 
718
            else:
 
719
                # if we finished all children, pop it off the stack
 
720
                stack.pop()
 
721
 
 
722
    def _preload_cache(self):
 
723
        """Populate any caches, we are about to access all items.
 
724
 
 
725
        The default implementation does nothing, because CommonInventory doesn't
 
726
        have a cache.
 
727
        """
 
728
        pass
 
729
 
 
730
    def iter_entries_by_dir(self, from_dir=None, specific_file_ids=None):
 
731
        """Iterate over the entries in a directory first order.
 
732
 
 
733
        This returns all entries for a directory before returning
 
734
        the entries for children of a directory. This is not
 
735
        lexicographically sorted order, and is a hybrid between
 
736
        depth-first and breadth-first.
 
737
 
 
738
        :return: This yields (path, entry) pairs
 
739
        """
 
740
        if specific_file_ids and not isinstance(specific_file_ids, set):
 
741
            specific_file_ids = set(specific_file_ids)
 
742
        # TODO? Perhaps this should return the from_dir so that the root is
 
743
        # yielded? or maybe an option?
 
744
        if from_dir is None and specific_file_ids is None:
 
745
            # They are iterating from the root, and have not specified any
 
746
            # specific entries to look at. All current callers fully consume the
 
747
            # iterator, so we can safely assume we are accessing all entries
 
748
            self._preload_cache()
 
749
        if from_dir is None:
 
750
            if self.root is None:
 
751
                return
 
752
            # Optimize a common case
 
753
            if (specific_file_ids is not None
 
754
                    and len(specific_file_ids) == 1):
 
755
                file_id = list(specific_file_ids)[0]
 
756
                if file_id is not None:
 
757
                    try:
 
758
                        path = self.id2path(file_id)
 
759
                    except errors.NoSuchId:
 
760
                        pass
 
761
                    else:
 
762
                        yield path, self.get_entry(file_id)
 
763
                return
 
764
            from_dir = self.root
 
765
            if (specific_file_ids is None
 
766
                    or self.root.file_id in specific_file_ids):
 
767
                yield u'', self.root
 
768
        elif isinstance(from_dir, bytes):
 
769
            from_dir = self.get_entry(from_dir)
 
770
        else:
 
771
            raise TypeError(from_dir)
 
772
 
 
773
        if specific_file_ids is not None:
 
774
            # TODO: jam 20070302 This could really be done as a loop rather
 
775
            #       than a bunch of recursive calls.
 
776
            parents = set()
 
777
            byid = self
 
778
 
 
779
            def add_ancestors(file_id):
 
780
                if not byid.has_id(file_id):
 
781
                    return
 
782
                parent_id = byid.get_entry(file_id).parent_id
 
783
                if parent_id is None:
 
784
                    return
 
785
                if parent_id not in parents:
 
786
                    parents.add(parent_id)
 
787
                    add_ancestors(parent_id)
 
788
            for file_id in specific_file_ids:
 
789
                add_ancestors(file_id)
 
790
        else:
 
791
            parents = None
 
792
 
 
793
        stack = [(u'', from_dir)]
 
794
        while stack:
 
795
            cur_relpath, cur_dir = stack.pop()
 
796
 
 
797
            child_dirs = []
 
798
            for child_name, child_ie in sorted(viewitems(cur_dir.children)):
 
799
 
 
800
                child_relpath = cur_relpath + child_name
 
801
 
 
802
                if (specific_file_ids is None
 
803
                        or child_ie.file_id in specific_file_ids):
 
804
                    yield child_relpath, child_ie
 
805
 
 
806
                if child_ie.kind == 'directory':
 
807
                    if parents is None or child_ie.file_id in parents:
 
808
                        child_dirs.append((child_relpath + '/', child_ie))
 
809
            stack.extend(reversed(child_dirs))
 
810
 
 
811
    def _make_delta(self, old):
 
812
        """Make an inventory delta from two inventories."""
 
813
        old_ids = set(old.iter_all_ids())
 
814
        new_ids = set(self.iter_all_ids())
 
815
        adds = new_ids - old_ids
 
816
        deletes = old_ids - new_ids
 
817
        common = old_ids.intersection(new_ids)
 
818
        delta = []
 
819
        for file_id in deletes:
 
820
            delta.append((old.id2path(file_id), None, file_id, None))
 
821
        for file_id in adds:
 
822
            delta.append((None, self.id2path(file_id),
 
823
                          file_id, self.get_entry(file_id)))
 
824
        for file_id in common:
 
825
            if old.get_entry(file_id) != self.get_entry(file_id):
 
826
                delta.append((old.id2path(file_id), self.id2path(file_id),
 
827
                              file_id, self.get_entry(file_id)))
 
828
        return delta
 
829
 
 
830
    def make_entry(self, kind, name, parent_id, file_id=None):
 
831
        """Simple thunk to breezy.bzr.inventory.make_entry."""
 
832
        return make_entry(kind, name, parent_id, file_id)
 
833
 
 
834
    def entries(self):
 
835
        """Return list of (path, ie) for all entries except the root.
 
836
 
 
837
        This may be faster than iter_entries.
 
838
        """
 
839
        accum = []
 
840
 
 
841
        def descend(dir_ie, dir_path):
 
842
            kids = sorted(viewitems(dir_ie.children))
 
843
            for name, ie in kids:
 
844
                child_path = osutils.pathjoin(dir_path, name)
 
845
                accum.append((child_path, ie))
 
846
                if ie.kind == 'directory':
 
847
                    descend(ie, child_path)
 
848
 
 
849
        if self.root is not None:
 
850
            descend(self.root, u'')
 
851
        return accum
 
852
 
 
853
    def get_entry_by_path(self, relpath):
 
854
        """Return an inventory entry by path.
 
855
 
 
856
        :param relpath: may be either a list of path components, or a single
 
857
            string, in which case it is automatically split.
 
858
 
 
859
        This returns the entry of the last component in the path,
 
860
        which may be either a file or a directory.
 
861
 
 
862
        Returns None IFF the path is not found.
 
863
        """
 
864
        if isinstance(relpath, (str, text_type)):
 
865
            names = osutils.splitpath(relpath)
 
866
        else:
 
867
            names = relpath
 
868
 
 
869
        try:
 
870
            parent = self.root
 
871
        except errors.NoSuchId:
 
872
            # root doesn't exist yet so nothing else can
 
873
            return None
 
874
        if parent is None:
 
875
            return None
 
876
        for f in names:
 
877
            try:
 
878
                children = getattr(parent, 'children', None)
 
879
                if children is None:
 
880
                    return None
 
881
                cie = children[f]
 
882
                parent = cie
 
883
            except KeyError:
 
884
                # or raise an error?
 
885
                return None
 
886
        return parent
 
887
 
 
888
    def path2id(self, relpath):
 
889
        """Walk down through directories to return entry of last component.
 
890
 
 
891
        :param relpath: may be either a list of path components, or a single
 
892
            string, in which case it is automatically split.
 
893
 
 
894
        This returns the entry of the last component in the path,
 
895
        which may be either a file or a directory.
 
896
 
 
897
        Returns None IFF the path is not found.
 
898
        """
 
899
        ie = self.get_entry_by_path(relpath)
 
900
        if ie is None:
 
901
            return None
 
902
        return ie.file_id
 
903
 
 
904
    def filter(self, specific_fileids):
 
905
        """Get an inventory view filtered against a set of file-ids.
 
906
 
 
907
        Children of directories and parents are included.
 
908
 
 
909
        The result may or may not reference the underlying inventory
 
910
        so it should be treated as immutable.
 
911
        """
 
912
        interesting_parents = set()
 
913
        for fileid in specific_fileids:
 
914
            try:
 
915
                interesting_parents.update(self.get_idpath(fileid))
 
916
            except errors.NoSuchId:
 
917
                # This fileid is not in the inventory - that's ok
 
918
                pass
 
919
        entries = self.iter_entries()
 
920
        if self.root is None:
 
921
            return Inventory(root_id=None)
 
922
        other = Inventory(next(entries)[1].file_id)
 
923
        other.root.revision = self.root.revision
 
924
        other.revision_id = self.revision_id
 
925
        directories_to_expand = set()
 
926
        for path, entry in entries:
 
927
            file_id = entry.file_id
 
928
            if (file_id in specific_fileids or
 
929
                    entry.parent_id in directories_to_expand):
 
930
                if entry.kind == 'directory':
 
931
                    directories_to_expand.add(file_id)
 
932
            elif file_id not in interesting_parents:
 
933
                continue
 
934
            other.add(entry.copy())
 
935
        return other
 
936
 
 
937
    def get_idpath(self, file_id):
 
938
        """Return a list of file_ids for the path to an entry.
 
939
 
 
940
        The list contains one element for each directory followed by
 
941
        the id of the file itself.  So the length of the returned list
 
942
        is equal to the depth of the file in the tree, counting the
 
943
        root directory as depth 1.
 
944
        """
 
945
        p = []
 
946
        for parent in self._iter_file_id_parents(file_id):
 
947
            p.insert(0, parent.file_id)
 
948
        return p
 
949
 
 
950
 
 
951
class Inventory(CommonInventory):
 
952
    """Mutable dict based in-memory inventory.
 
953
 
 
954
    We never store the full path to a file, because renaming a directory
 
955
    implicitly moves all of its contents.  This class internally maintains a
 
956
    lookup tree that allows the children under a directory to be
 
957
    returned quickly.
 
958
 
 
959
    >>> inv = Inventory()
 
960
    >>> inv.add(InventoryFile(b'123-123', 'hello.c', ROOT_ID))
 
961
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
 
962
    >>> inv.get_entry(b'123-123').name
 
963
    'hello.c'
 
964
 
 
965
    Id's may be looked up from paths:
 
966
 
 
967
    >>> inv.path2id('hello.c')
 
968
    '123-123'
 
969
    >>> inv.has_id(b'123-123')
 
970
    True
 
971
 
 
972
    There are iterators over the contents:
 
973
 
 
974
    >>> [entry[0] for entry in inv.iter_entries()]
 
975
    ['', u'hello.c']
 
976
    """
 
977
 
 
978
    def __init__(self, root_id=ROOT_ID, revision_id=None):
 
979
        """Create or read an inventory.
 
980
 
 
981
        If a working directory is specified, the inventory is read
 
982
        from there.  If the file is specified, read from that. If not,
 
983
        the inventory is created empty.
 
984
 
 
985
        The inventory is created with a default root directory, with
 
986
        an id of None.
 
987
        """
 
988
        if root_id is not None:
 
989
            self._set_root(InventoryDirectory(root_id, u'', None))
 
990
        else:
 
991
            self.root = None
 
992
            self._byid = {}
 
993
        self.revision_id = revision_id
 
994
 
 
995
    def __repr__(self):
 
996
        # More than one page of ouput is not useful anymore to debug
 
997
        max_len = 2048
 
998
        closing = '...}'
 
999
        contents = repr(self._byid)
 
1000
        if len(contents) > max_len:
 
1001
            contents = contents[:(max_len - len(closing))] + closing
 
1002
        return "<Inventory object at %x, contents=%r>" % (id(self), contents)
 
1003
 
 
1004
    def apply_delta(self, delta):
 
1005
        """Apply a delta to this inventory.
 
1006
 
 
1007
        See the inventory developers documentation for the theory behind
 
1008
        inventory deltas.
 
1009
 
 
1010
        If delta application fails the inventory is left in an indeterminate
 
1011
        state and must not be used.
 
1012
 
 
1013
        :param delta: A list of changes to apply. After all the changes are
 
1014
            applied the final inventory must be internally consistent, but it
 
1015
            is ok to supply changes which, if only half-applied would have an
 
1016
            invalid result - such as supplying two changes which rename two
 
1017
            files, 'A' and 'B' with each other : [('A', 'B', b'A-id', a_entry),
 
1018
            ('B', 'A', b'B-id', b_entry)].
 
1019
 
 
1020
            Each change is a tuple, of the form (old_path, new_path, file_id,
 
1021
            new_entry).
 
1022
 
 
1023
            When new_path is None, the change indicates the removal of an entry
 
1024
            from the inventory and new_entry will be ignored (using None is
 
1025
            appropriate). If new_path is not None, then new_entry must be an
 
1026
            InventoryEntry instance, which will be incorporated into the
 
1027
            inventory (and replace any existing entry with the same file id).
 
1028
 
 
1029
            When old_path is None, the change indicates the addition of
 
1030
            a new entry to the inventory.
 
1031
 
 
1032
            When neither new_path nor old_path are None, the change is a
 
1033
            modification to an entry, such as a rename, reparent, kind change
 
1034
            etc.
 
1035
 
 
1036
            The children attribute of new_entry is ignored. This is because
 
1037
            this method preserves children automatically across alterations to
 
1038
            the parent of the children, and cases where the parent id of a
 
1039
            child is changing require the child to be passed in as a separate
 
1040
            change regardless. E.g. in the recursive deletion of a directory -
 
1041
            the directory's children must be included in the delta, or the
 
1042
            final inventory will be invalid.
 
1043
 
 
1044
            Note that a file_id must only appear once within a given delta.
 
1045
            An AssertionError is raised otherwise.
 
1046
        """
 
1047
        # Check that the delta is legal. It would be nice if this could be
 
1048
        # done within the loops below but it's safer to validate the delta
 
1049
        # before starting to mutate the inventory, as there isn't a rollback
 
1050
        # facility.
 
1051
        list(_check_delta_unique_ids(_check_delta_unique_new_paths(
 
1052
            _check_delta_unique_old_paths(_check_delta_ids_match_entry(
 
1053
                _check_delta_ids_are_valid(
 
1054
                    _check_delta_new_path_entry_both_or_None(
 
1055
                        delta)))))))
 
1056
 
 
1057
        children = {}
 
1058
        # Remove all affected items which were in the original inventory,
 
1059
        # starting with the longest paths, thus ensuring parents are examined
 
1060
        # after their children, which means that everything we examine has no
 
1061
        # modified children remaining by the time we examine it.
 
1062
        for old_path, file_id in sorted(((op, f) for op, np, f, e in delta
 
1063
                                         if op is not None), reverse=True):
 
1064
            # Preserve unaltered children of file_id for later reinsertion.
 
1065
            file_id_children = getattr(self.get_entry(file_id), 'children', {})
 
1066
            if len(file_id_children):
 
1067
                children[file_id] = file_id_children
 
1068
            if self.id2path(file_id) != old_path:
 
1069
                raise errors.InconsistentDelta(old_path, file_id,
 
1070
                                               "Entry was at wrong other path %r." % self.id2path(file_id))
 
1071
            # Remove file_id and the unaltered children. If file_id is not
 
1072
            # being deleted it will be reinserted back later.
 
1073
            self.remove_recursive_id(file_id)
 
1074
        # Insert all affected which should be in the new inventory, reattaching
 
1075
        # their children if they had any. This is done from shortest path to
 
1076
        # longest, ensuring that items which were modified and whose parents in
 
1077
        # the resulting inventory were also modified, are inserted after their
 
1078
        # parents.
 
1079
        for new_path, f, new_entry in sorted((np, f, e) for op, np, f, e in
 
1080
                                             delta if np is not None):
 
1081
            if new_entry.kind == 'directory':
 
1082
                # Pop the child which to allow detection of children whose
 
1083
                # parents were deleted and which were not reattached to a new
 
1084
                # parent.
 
1085
                replacement = InventoryDirectory(new_entry.file_id,
 
1086
                                                 new_entry.name, new_entry.parent_id)
 
1087
                replacement.revision = new_entry.revision
 
1088
                replacement.children = children.pop(replacement.file_id, {})
 
1089
                new_entry = replacement
 
1090
            try:
 
1091
                self.add(new_entry)
 
1092
            except errors.DuplicateFileId:
 
1093
                raise errors.InconsistentDelta(new_path, new_entry.file_id,
 
1094
                                               "New id is already present in target.")
 
1095
            except AttributeError:
 
1096
                raise errors.InconsistentDelta(new_path, new_entry.file_id,
 
1097
                                               "Parent is not a directory.")
 
1098
            if self.id2path(new_entry.file_id) != new_path:
 
1099
                raise errors.InconsistentDelta(new_path, new_entry.file_id,
 
1100
                                               "New path is not consistent with parent path.")
 
1101
        if len(children):
 
1102
            # Get the parent id that was deleted
 
1103
            parent_id, children = children.popitem()
 
1104
            raise errors.InconsistentDelta("<deleted>", parent_id,
 
1105
                                           "The file id was deleted but its children were not deleted.")
 
1106
 
 
1107
    def create_by_apply_delta(self, inventory_delta, new_revision_id,
 
1108
                              propagate_caches=False):
 
1109
        """See CHKInventory.create_by_apply_delta()"""
 
1110
        new_inv = self.copy()
 
1111
        new_inv.apply_delta(inventory_delta)
 
1112
        new_inv.revision_id = new_revision_id
 
1113
        return new_inv
 
1114
 
 
1115
    def _set_root(self, ie):
 
1116
        self.root = ie
 
1117
        self._byid = {self.root.file_id: self.root}
 
1118
 
 
1119
    def copy(self):
 
1120
        # TODO: jam 20051218 Should copy also copy the revision_id?
 
1121
        entries = self.iter_entries()
 
1122
        if self.root is None:
 
1123
            return Inventory(root_id=None)
 
1124
        other = Inventory(next(entries)[1].file_id)
 
1125
        other.root.revision = self.root.revision
 
1126
        # copy recursively so we know directories will be added before
 
1127
        # their children.  There are more efficient ways than this...
 
1128
        for path, entry in entries:
 
1129
            other.add(entry.copy())
 
1130
        return other
 
1131
 
 
1132
    def iter_all_ids(self):
 
1133
        """Iterate over all file-ids."""
 
1134
        return iter(self._byid)
 
1135
 
 
1136
    def iter_just_entries(self):
 
1137
        """Iterate over all entries.
 
1138
 
 
1139
        Unlike iter_entries(), just the entries are returned (not (path, ie))
 
1140
        and the order of entries is undefined.
 
1141
 
 
1142
        XXX: We may not want to merge this into bzr.dev.
 
1143
        """
 
1144
        if self.root is None:
 
1145
            return ()
 
1146
        return iter(viewvalues(self._byid))
 
1147
 
 
1148
    def __len__(self):
 
1149
        """Returns number of entries."""
 
1150
        return len(self._byid)
 
1151
 
 
1152
    def get_entry(self, file_id):
 
1153
        """Return the entry for given file_id.
 
1154
 
 
1155
        >>> inv = Inventory()
 
1156
        >>> inv.add(InventoryFile(b'123123', 'hello.c', ROOT_ID))
 
1157
        InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
 
1158
        >>> inv.get_entry(b'123123').name
 
1159
        'hello.c'
 
1160
        """
 
1161
        if not isinstance(file_id, bytes):
 
1162
            raise TypeError(file_id)
 
1163
        try:
 
1164
            return self._byid[file_id]
 
1165
        except KeyError:
 
1166
            # really we're passing an inventory, not a tree...
 
1167
            raise errors.NoSuchId(self, file_id)
 
1168
 
 
1169
    def get_file_kind(self, file_id):
 
1170
        return self._byid[file_id].kind
 
1171
 
 
1172
    def get_child(self, parent_id, filename):
 
1173
        return self.get_entry(parent_id).children.get(filename)
 
1174
 
 
1175
    def _add_child(self, entry):
 
1176
        """Add an entry to the inventory, without adding it to its parent"""
 
1177
        if entry.file_id in self._byid:
 
1178
            raise errors.BzrError(
 
1179
                "inventory already contains entry with id {%s}" %
 
1180
                entry.file_id)
 
1181
        self._byid[entry.file_id] = entry
 
1182
        children = getattr(entry, 'children', {})
 
1183
        if children is not None:
 
1184
            for child in viewvalues(children):
 
1185
                self._add_child(child)
 
1186
        return entry
 
1187
 
 
1188
    def add(self, entry):
 
1189
        """Add entry to inventory.
 
1190
 
 
1191
        :return: entry
 
1192
        """
 
1193
        if entry.file_id in self._byid:
 
1194
            raise errors.DuplicateFileId(entry.file_id,
 
1195
                                         self._byid[entry.file_id])
 
1196
        if entry.parent_id is None:
 
1197
            self.root = entry
 
1198
        else:
 
1199
            try:
 
1200
                parent = self._byid[entry.parent_id]
 
1201
            except KeyError:
 
1202
                raise errors.InconsistentDelta("<unknown>", entry.parent_id,
 
1203
                                               "Parent not in inventory.")
 
1204
            if entry.name in parent.children:
 
1205
                raise errors.InconsistentDelta(
 
1206
                    self.id2path(parent.children[entry.name].file_id),
 
1207
                    entry.file_id,
 
1208
                    "Path already versioned")
 
1209
            parent.children[entry.name] = entry
 
1210
        return self._add_child(entry)
 
1211
 
 
1212
    def add_path(self, relpath, kind, file_id=None, parent_id=None):
 
1213
        """Add entry from a path.
 
1214
 
 
1215
        The immediate parent must already be versioned.
 
1216
 
 
1217
        Returns the new entry object."""
 
1218
 
 
1219
        parts = osutils.splitpath(relpath)
 
1220
 
 
1221
        if len(parts) == 0:
 
1222
            if file_id is None:
 
1223
                file_id = generate_ids.gen_root_id()
 
1224
            self.root = InventoryDirectory(file_id, '', None)
 
1225
            self._byid = {self.root.file_id: self.root}
 
1226
            return self.root
 
1227
        else:
 
1228
            parent_path = parts[:-1]
 
1229
            parent_id = self.path2id(parent_path)
 
1230
            if parent_id is None:
 
1231
                raise errors.NotVersionedError(path=parent_path)
 
1232
        ie = make_entry(kind, parts[-1], parent_id, file_id)
 
1233
        return self.add(ie)
 
1234
 
 
1235
    def delete(self, file_id):
 
1236
        """Remove entry by id.
 
1237
 
 
1238
        >>> inv = Inventory()
 
1239
        >>> inv.add(InventoryFile(b'123', 'foo.c', ROOT_ID))
 
1240
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
 
1241
        >>> inv.has_id(b'123')
 
1242
        True
 
1243
        >>> inv.delete(b'123')
 
1244
        >>> inv.has_id(b'123')
 
1245
        False
 
1246
        """
 
1247
        ie = self.get_entry(file_id)
 
1248
        del self._byid[file_id]
 
1249
        if ie.parent_id is not None:
 
1250
            del self.get_entry(ie.parent_id).children[ie.name]
 
1251
 
 
1252
    def __eq__(self, other):
 
1253
        """Compare two sets by comparing their contents.
 
1254
 
 
1255
        >>> i1 = Inventory()
 
1256
        >>> i2 = Inventory()
 
1257
        >>> i1 == i2
 
1258
        True
 
1259
        >>> i1.add(InventoryFile(b'123', 'foo', ROOT_ID))
 
1260
        InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
 
1261
        >>> i1 == i2
 
1262
        False
 
1263
        >>> i2.add(InventoryFile(b'123', 'foo', ROOT_ID))
 
1264
        InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
 
1265
        >>> i1 == i2
 
1266
        True
 
1267
        """
 
1268
        if not isinstance(other, Inventory):
 
1269
            return NotImplemented
 
1270
 
 
1271
        return self._byid == other._byid
 
1272
 
 
1273
    def __ne__(self, other):
 
1274
        return not self.__eq__(other)
 
1275
 
 
1276
    def __hash__(self):
 
1277
        raise ValueError('not hashable')
 
1278
 
 
1279
    def _iter_file_id_parents(self, file_id):
 
1280
        """Yield the parents of file_id up to the root."""
 
1281
        while file_id is not None:
 
1282
            try:
 
1283
                ie = self._byid[file_id]
 
1284
            except KeyError:
 
1285
                raise errors.NoSuchId(tree=None, file_id=file_id)
 
1286
            yield ie
 
1287
            file_id = ie.parent_id
 
1288
 
 
1289
    def has_id(self, file_id):
 
1290
        return (file_id in self._byid)
 
1291
 
 
1292
    def _make_delta(self, old):
 
1293
        """Make an inventory delta from two inventories."""
 
1294
        old_getter = old.get_entry
 
1295
        new_getter = self.get_entry
 
1296
        old_ids = set(old.iter_all_ids())
 
1297
        new_ids = set(self.iter_all_ids())
 
1298
        adds = new_ids - old_ids
 
1299
        deletes = old_ids - new_ids
 
1300
        if not adds and not deletes:
 
1301
            common = new_ids
 
1302
        else:
 
1303
            common = old_ids.intersection(new_ids)
 
1304
        delta = []
 
1305
        for file_id in deletes:
 
1306
            delta.append((old.id2path(file_id), None, file_id, None))
 
1307
        for file_id in adds:
 
1308
            delta.append((None, self.id2path(file_id),
 
1309
                          file_id, self.get_entry(file_id)))
 
1310
        for file_id in common:
 
1311
            new_ie = new_getter(file_id)
 
1312
            old_ie = old_getter(file_id)
 
1313
            # If xml_serializer returns the cached InventoryEntries (rather
 
1314
            # than always doing .copy()), inlining the 'is' check saves 2.7M
 
1315
            # calls to __eq__.  Under lsprof this saves 20s => 6s.
 
1316
            # It is a minor improvement without lsprof.
 
1317
            if old_ie is new_ie or old_ie == new_ie:
 
1318
                continue
 
1319
            else:
 
1320
                delta.append((old.id2path(file_id), self.id2path(file_id),
 
1321
                              file_id, new_ie))
 
1322
        return delta
 
1323
 
 
1324
    def remove_recursive_id(self, file_id):
 
1325
        """Remove file_id, and children, from the inventory.
 
1326
 
 
1327
        :param file_id: A file_id to remove.
 
1328
        """
 
1329
        to_find_delete = [self._byid[file_id]]
 
1330
        to_delete = []
 
1331
        while to_find_delete:
 
1332
            ie = to_find_delete.pop()
 
1333
            to_delete.append(ie.file_id)
 
1334
            if ie.kind == 'directory':
 
1335
                to_find_delete.extend(viewvalues(ie.children))
 
1336
        for file_id in reversed(to_delete):
 
1337
            ie = self.get_entry(file_id)
 
1338
            del self._byid[file_id]
 
1339
        if ie.parent_id is not None:
 
1340
            del self.get_entry(ie.parent_id).children[ie.name]
 
1341
        else:
 
1342
            self.root = None
 
1343
 
 
1344
    def rename(self, file_id, new_parent_id, new_name):
 
1345
        """Move a file within the inventory.
 
1346
 
 
1347
        This can change either the name, or the parent, or both.
 
1348
 
 
1349
        This does not move the working file.
 
1350
        """
 
1351
        new_name = ensure_normalized_name(new_name)
 
1352
        if not is_valid_name(new_name):
 
1353
            raise errors.BzrError("not an acceptable filename: %r" % new_name)
 
1354
 
 
1355
        new_parent = self._byid[new_parent_id]
 
1356
        if new_name in new_parent.children:
 
1357
            raise errors.BzrError("%r already exists in %r" %
 
1358
                                  (new_name, self.id2path(new_parent_id)))
 
1359
 
 
1360
        new_parent_idpath = self.get_idpath(new_parent_id)
 
1361
        if file_id in new_parent_idpath:
 
1362
            raise errors.BzrError(
 
1363
                "cannot move directory %r into a subdirectory of itself, %r"
 
1364
                % (self.id2path(file_id), self.id2path(new_parent_id)))
 
1365
 
 
1366
        file_ie = self._byid[file_id]
 
1367
        old_parent = self._byid[file_ie.parent_id]
 
1368
 
 
1369
        # TODO: Don't leave things messed up if this fails
 
1370
 
 
1371
        del old_parent.children[file_ie.name]
 
1372
        new_parent.children[new_name] = file_ie
 
1373
 
 
1374
        file_ie.name = new_name
 
1375
        file_ie.parent_id = new_parent_id
 
1376
 
 
1377
    def is_root(self, file_id):
 
1378
        return self.root is not None and file_id == self.root.file_id
 
1379
 
 
1380
 
 
1381
class CHKInventory(CommonInventory):
 
1382
    """An inventory persisted in a CHK store.
 
1383
 
 
1384
    By design, a CHKInventory is immutable so many of the methods
 
1385
    supported by Inventory - add, rename, apply_delta, etc - are *not*
 
1386
    supported. To create a new CHKInventory, use create_by_apply_delta()
 
1387
    or from_inventory(), say.
 
1388
 
 
1389
    Internally, a CHKInventory has one or two CHKMaps:
 
1390
 
 
1391
    * id_to_entry - a map from (file_id,) => InventoryEntry as bytes
 
1392
    * parent_id_basename_to_file_id - a map from (parent_id, basename_utf8)
 
1393
        => file_id as bytes
 
1394
 
 
1395
    The second map is optional and not present in early CHkRepository's.
 
1396
 
 
1397
    No caching is performed: every method call or item access will perform
 
1398
    requests to the storage layer. As such, keep references to objects you
 
1399
    want to reuse.
 
1400
    """
 
1401
 
 
1402
    def __init__(self, search_key_name):
 
1403
        CommonInventory.__init__(self)
 
1404
        self._fileid_to_entry_cache = {}
 
1405
        self._fully_cached = False
 
1406
        self._path_to_fileid_cache = {}
 
1407
        self._search_key_name = search_key_name
 
1408
        self.root_id = None
 
1409
 
 
1410
    def __eq__(self, other):
 
1411
        """Compare two sets by comparing their contents."""
 
1412
        if not isinstance(other, CHKInventory):
 
1413
            return NotImplemented
 
1414
 
 
1415
        this_key = self.id_to_entry.key()
 
1416
        other_key = other.id_to_entry.key()
 
1417
        this_pid_key = self.parent_id_basename_to_file_id.key()
 
1418
        other_pid_key = other.parent_id_basename_to_file_id.key()
 
1419
        if None in (this_key, this_pid_key, other_key, other_pid_key):
 
1420
            return False
 
1421
        return this_key == other_key and this_pid_key == other_pid_key
 
1422
 
 
1423
    def _entry_to_bytes(self, entry):
 
1424
        """Serialise entry as a single bytestring.
 
1425
 
 
1426
        :param Entry: An inventory entry.
 
1427
        :return: A bytestring for the entry.
 
1428
 
 
1429
        The BNF:
 
1430
        ENTRY ::= FILE | DIR | SYMLINK | TREE
 
1431
        FILE ::= "file: " COMMON SEP SHA SEP SIZE SEP EXECUTABLE
 
1432
        DIR ::= "dir: " COMMON
 
1433
        SYMLINK ::= "symlink: " COMMON SEP TARGET_UTF8
 
1434
        TREE ::= "tree: " COMMON REFERENCE_REVISION
 
1435
        COMMON ::= FILE_ID SEP PARENT_ID SEP NAME_UTF8 SEP REVISION
 
1436
        SEP ::= "\n"
 
1437
        """
 
1438
        if entry.parent_id is not None:
 
1439
            parent_str = entry.parent_id
 
1440
        else:
 
1441
            parent_str = b''
 
1442
        name_str = entry.name.encode("utf8")
 
1443
        if entry.kind == 'file':
 
1444
            if entry.executable:
 
1445
                exec_str = b"Y"
 
1446
            else:
 
1447
                exec_str = b"N"
 
1448
            return b"file: %s\n%s\n%s\n%s\n%s\n%d\n%s" % (
 
1449
                entry.file_id, parent_str, name_str, entry.revision,
 
1450
                entry.text_sha1, entry.text_size, exec_str)
 
1451
        elif entry.kind == 'directory':
 
1452
            return b"dir: %s\n%s\n%s\n%s" % (
 
1453
                entry.file_id, parent_str, name_str, entry.revision)
 
1454
        elif entry.kind == 'symlink':
 
1455
            return b"symlink: %s\n%s\n%s\n%s\n%s" % (
 
1456
                entry.file_id, parent_str, name_str, entry.revision,
 
1457
                entry.symlink_target.encode("utf8"))
 
1458
        elif entry.kind == 'tree-reference':
 
1459
            return b"tree: %s\n%s\n%s\n%s\n%s" % (
 
1460
                entry.file_id, parent_str, name_str, entry.revision,
 
1461
                entry.reference_revision)
 
1462
        else:
 
1463
            raise ValueError("unknown kind %r" % entry.kind)
 
1464
 
 
1465
    def _expand_fileids_to_parents_and_children(self, file_ids):
 
1466
        """Give a more wholistic view starting with the given file_ids.
 
1467
 
 
1468
        For any file_id which maps to a directory, we will include all children
 
1469
        of that directory. We will also include all directories which are
 
1470
        parents of the given file_ids, but we will not include their children.
 
1471
 
 
1472
        eg:
 
1473
          /     # TREE_ROOT
 
1474
          foo/  # foo-id
 
1475
            baz # baz-id
 
1476
            frob/ # frob-id
 
1477
              fringle # fringle-id
 
1478
          bar/  # bar-id
 
1479
            bing # bing-id
 
1480
 
 
1481
        if given [foo-id] we will include
 
1482
            TREE_ROOT as interesting parents
 
1483
        and 
 
1484
            foo-id, baz-id, frob-id, fringle-id
 
1485
        As interesting ids.
 
1486
        """
 
1487
        interesting = set()
 
1488
        # TODO: Pre-pass over the list of fileids to see if anything is already
 
1489
        #       deserialized in self._fileid_to_entry_cache
 
1490
 
 
1491
        directories_to_expand = set()
 
1492
        children_of_parent_id = {}
 
1493
        # It is okay if some of the fileids are missing
 
1494
        for entry in self._getitems(file_ids):
 
1495
            if entry.kind == 'directory':
 
1496
                directories_to_expand.add(entry.file_id)
 
1497
            interesting.add(entry.parent_id)
 
1498
            children_of_parent_id.setdefault(entry.parent_id, set()
 
1499
                                             ).add(entry.file_id)
 
1500
 
 
1501
        # Now, interesting has all of the direct parents, but not the
 
1502
        # parents of those parents. It also may have some duplicates with
 
1503
        # specific_fileids
 
1504
        remaining_parents = interesting.difference(file_ids)
 
1505
        # When we hit the TREE_ROOT, we'll get an interesting parent of None,
 
1506
        # but we don't actually want to recurse into that
 
1507
        interesting.add(None)  # this will auto-filter it in the loop
 
1508
        remaining_parents.discard(None)
 
1509
        while remaining_parents:
 
1510
            next_parents = set()
 
1511
            for entry in self._getitems(remaining_parents):
 
1512
                next_parents.add(entry.parent_id)
 
1513
                children_of_parent_id.setdefault(entry.parent_id, set()
 
1514
                                                 ).add(entry.file_id)
 
1515
            # Remove any search tips we've already processed
 
1516
            remaining_parents = next_parents.difference(interesting)
 
1517
            interesting.update(remaining_parents)
 
1518
            # We should probably also .difference(directories_to_expand)
 
1519
        interesting.update(file_ids)
 
1520
        interesting.discard(None)
 
1521
        while directories_to_expand:
 
1522
            # Expand directories by looking in the
 
1523
            # parent_id_basename_to_file_id map
 
1524
            keys = [StaticTuple(f,).intern() for f in directories_to_expand]
 
1525
            directories_to_expand = set()
 
1526
            items = self.parent_id_basename_to_file_id.iteritems(keys)
 
1527
            next_file_ids = {item[1] for item in items}
 
1528
            next_file_ids = next_file_ids.difference(interesting)
 
1529
            interesting.update(next_file_ids)
 
1530
            for entry in self._getitems(next_file_ids):
 
1531
                if entry.kind == 'directory':
 
1532
                    directories_to_expand.add(entry.file_id)
 
1533
                children_of_parent_id.setdefault(entry.parent_id, set()
 
1534
                                                 ).add(entry.file_id)
 
1535
        return interesting, children_of_parent_id
 
1536
 
 
1537
    def filter(self, specific_fileids):
 
1538
        """Get an inventory view filtered against a set of file-ids.
 
1539
 
 
1540
        Children of directories and parents are included.
 
1541
 
 
1542
        The result may or may not reference the underlying inventory
 
1543
        so it should be treated as immutable.
 
1544
        """
 
1545
        (interesting,
 
1546
         parent_to_children) = self._expand_fileids_to_parents_and_children(
 
1547
            specific_fileids)
 
1548
        # There is some overlap here, but we assume that all interesting items
 
1549
        # are in the _fileid_to_entry_cache because we had to read them to
 
1550
        # determine if they were a dir we wanted to recurse, or just a file
 
1551
        # This should give us all the entries we'll want to add, so start
 
1552
        # adding
 
1553
        other = Inventory(self.root_id)
 
1554
        other.root.revision = self.root.revision
 
1555
        other.revision_id = self.revision_id
 
1556
        if not interesting or not parent_to_children:
 
1557
            # empty filter, or filtering entrys that don't exist
 
1558
            # (if even 1 existed, then we would have populated
 
1559
            # parent_to_children with at least the tree root.)
 
1560
            return other
 
1561
        cache = self._fileid_to_entry_cache
 
1562
        remaining_children = collections.deque(
 
1563
            parent_to_children[self.root_id])
 
1564
        while remaining_children:
 
1565
            file_id = remaining_children.popleft()
 
1566
            ie = cache[file_id]
 
1567
            if ie.kind == 'directory':
 
1568
                ie = ie.copy()  # We create a copy to depopulate the .children attribute
 
1569
            # TODO: depending on the uses of 'other' we should probably alwyas
 
1570
            #       '.copy()' to prevent someone from mutating other and
 
1571
            #       invaliding our internal cache
 
1572
            other.add(ie)
 
1573
            if file_id in parent_to_children:
 
1574
                remaining_children.extend(parent_to_children[file_id])
 
1575
        return other
 
1576
 
 
1577
    @staticmethod
 
1578
    def _bytes_to_utf8name_key(data):
 
1579
        """Get the file_id, revision_id key out of data."""
 
1580
        # We don't normally care about name, except for times when we want
 
1581
        # to filter out empty names because of non rich-root...
 
1582
        sections = data.split(b'\n')
 
1583
        kind, file_id = sections[0].split(b': ')
 
1584
        return (sections[2], bytesintern(file_id), bytesintern(sections[3]))
 
1585
 
 
1586
    def _bytes_to_entry(self, bytes):
 
1587
        """Deserialise a serialised entry."""
 
1588
        sections = bytes.split(b'\n')
 
1589
        if sections[0].startswith(b"file: "):
 
1590
            result = InventoryFile(sections[0][6:],
 
1591
                                   sections[2].decode('utf8'),
 
1592
                                   sections[1])
 
1593
            result.text_sha1 = sections[4]
 
1594
            result.text_size = int(sections[5])
 
1595
            result.executable = sections[6] == b"Y"
 
1596
        elif sections[0].startswith(b"dir: "):
 
1597
            result = CHKInventoryDirectory(sections[0][5:],
 
1598
                                           sections[2].decode('utf8'),
 
1599
                                           sections[1], self)
 
1600
        elif sections[0].startswith(b"symlink: "):
 
1601
            result = InventoryLink(sections[0][9:],
 
1602
                                   sections[2].decode('utf8'),
 
1603
                                   sections[1])
 
1604
            result.symlink_target = sections[4].decode('utf8')
 
1605
        elif sections[0].startswith(b"tree: "):
 
1606
            result = TreeReference(sections[0][6:],
 
1607
                                   sections[2].decode('utf8'),
 
1608
                                   sections[1])
 
1609
            result.reference_revision = sections[4]
 
1610
        else:
 
1611
            raise ValueError("Not a serialised entry %r" % bytes)
 
1612
        result.file_id = bytesintern(result.file_id)
 
1613
        result.revision = bytesintern(sections[3])
 
1614
        if result.parent_id == b'':
 
1615
            result.parent_id = None
 
1616
        self._fileid_to_entry_cache[result.file_id] = result
 
1617
        return result
 
1618
 
 
1619
    def create_by_apply_delta(self, inventory_delta, new_revision_id,
 
1620
                              propagate_caches=False):
 
1621
        """Create a new CHKInventory by applying inventory_delta to this one.
 
1622
 
 
1623
        See the inventory developers documentation for the theory behind
 
1624
        inventory deltas.
 
1625
 
 
1626
        :param inventory_delta: The inventory delta to apply. See
 
1627
            Inventory.apply_delta for details.
 
1628
        :param new_revision_id: The revision id of the resulting CHKInventory.
 
1629
        :param propagate_caches: If True, the caches for this inventory are
 
1630
          copied to and updated for the result.
 
1631
        :return: The new CHKInventory.
 
1632
        """
 
1633
        split = osutils.split
 
1634
        result = CHKInventory(self._search_key_name)
 
1635
        if propagate_caches:
 
1636
            # Just propagate the path-to-fileid cache for now
 
1637
            result._path_to_fileid_cache = self._path_to_fileid_cache.copy()
 
1638
        search_key_func = chk_map.search_key_registry.get(
 
1639
            self._search_key_name)
 
1640
        self.id_to_entry._ensure_root()
 
1641
        maximum_size = self.id_to_entry._root_node.maximum_size
 
1642
        result.revision_id = new_revision_id
 
1643
        result.id_to_entry = chk_map.CHKMap(
 
1644
            self.id_to_entry._store,
 
1645
            self.id_to_entry.key(),
 
1646
            search_key_func=search_key_func)
 
1647
        result.id_to_entry._ensure_root()
 
1648
        result.id_to_entry._root_node.set_maximum_size(maximum_size)
 
1649
        # Change to apply to the parent_id_basename delta. The dict maps
 
1650
        # (parent_id, basename) -> (old_key, new_value). We use a dict because
 
1651
        # when a path has its id replaced (e.g. the root is changed, or someone
 
1652
        # does bzr mv a b, bzr mv c a, we should output a single change to this
 
1653
        # map rather than two.
 
1654
        parent_id_basename_delta = {}
 
1655
        if self.parent_id_basename_to_file_id is not None:
 
1656
            result.parent_id_basename_to_file_id = chk_map.CHKMap(
 
1657
                self.parent_id_basename_to_file_id._store,
 
1658
                self.parent_id_basename_to_file_id.key(),
 
1659
                search_key_func=search_key_func)
 
1660
            result.parent_id_basename_to_file_id._ensure_root()
 
1661
            self.parent_id_basename_to_file_id._ensure_root()
 
1662
            result_p_id_root = result.parent_id_basename_to_file_id._root_node
 
1663
            p_id_root = self.parent_id_basename_to_file_id._root_node
 
1664
            result_p_id_root.set_maximum_size(p_id_root.maximum_size)
 
1665
            result_p_id_root._key_width = p_id_root._key_width
 
1666
        else:
 
1667
            result.parent_id_basename_to_file_id = None
 
1668
        result.root_id = self.root_id
 
1669
        id_to_entry_delta = []
 
1670
        # inventory_delta is only traversed once, so we just update the
 
1671
        # variable.
 
1672
        # Check for repeated file ids
 
1673
        inventory_delta = _check_delta_unique_ids(inventory_delta)
 
1674
        # Repeated old paths
 
1675
        inventory_delta = _check_delta_unique_old_paths(inventory_delta)
 
1676
        # Check for repeated new paths
 
1677
        inventory_delta = _check_delta_unique_new_paths(inventory_delta)
 
1678
        # Check for entries that don't match the fileid
 
1679
        inventory_delta = _check_delta_ids_match_entry(inventory_delta)
 
1680
        # Check for nonsense fileids
 
1681
        inventory_delta = _check_delta_ids_are_valid(inventory_delta)
 
1682
        # Check for new_path <-> entry consistency
 
1683
        inventory_delta = _check_delta_new_path_entry_both_or_None(
 
1684
            inventory_delta)
 
1685
        # All changed entries need to have their parents be directories and be
 
1686
        # at the right path. This set contains (path, id) tuples.
 
1687
        parents = set()
 
1688
        # When we delete an item, all the children of it must be either deleted
 
1689
        # or altered in their own right. As we batch process the change via
 
1690
        # CHKMap.apply_delta, we build a set of things to use to validate the
 
1691
        # delta.
 
1692
        deletes = set()
 
1693
        altered = set()
 
1694
        for old_path, new_path, file_id, entry in inventory_delta:
 
1695
            # file id changes
 
1696
            if new_path == '':
 
1697
                result.root_id = file_id
 
1698
            if new_path is None:
 
1699
                # Make a delete:
 
1700
                new_key = None
 
1701
                new_value = None
 
1702
                # Update caches
 
1703
                if propagate_caches:
 
1704
                    try:
 
1705
                        del result._path_to_fileid_cache[old_path]
 
1706
                    except KeyError:
 
1707
                        pass
 
1708
                deletes.add(file_id)
 
1709
            else:
 
1710
                new_key = StaticTuple(file_id,)
 
1711
                new_value = result._entry_to_bytes(entry)
 
1712
                # Update caches. It's worth doing this whether
 
1713
                # we're propagating the old caches or not.
 
1714
                result._path_to_fileid_cache[new_path] = file_id
 
1715
                parents.add((split(new_path)[0], entry.parent_id))
 
1716
            if old_path is None:
 
1717
                old_key = None
 
1718
            else:
 
1719
                old_key = StaticTuple(file_id,)
 
1720
                if self.id2path(file_id) != old_path:
 
1721
                    raise errors.InconsistentDelta(old_path, file_id,
 
1722
                                                   "Entry was at wrong other path %r." %
 
1723
                                                   self.id2path(file_id))
 
1724
                altered.add(file_id)
 
1725
            id_to_entry_delta.append(StaticTuple(old_key, new_key, new_value))
 
1726
            if result.parent_id_basename_to_file_id is not None:
 
1727
                # parent_id, basename changes
 
1728
                if old_path is None:
 
1729
                    old_key = None
 
1730
                else:
 
1731
                    old_entry = self.get_entry(file_id)
 
1732
                    old_key = self._parent_id_basename_key(old_entry)
 
1733
                if new_path is None:
 
1734
                    new_key = None
 
1735
                    new_value = None
 
1736
                else:
 
1737
                    new_key = self._parent_id_basename_key(entry)
 
1738
                    new_value = file_id
 
1739
                # If the two keys are the same, the value will be unchanged
 
1740
                # as its always the file id for this entry.
 
1741
                if old_key != new_key:
 
1742
                    # Transform a change into explicit delete/add preserving
 
1743
                    # a possible match on the key from a different file id.
 
1744
                    if old_key is not None:
 
1745
                        parent_id_basename_delta.setdefault(
 
1746
                            old_key, [None, None])[0] = old_key
 
1747
                    if new_key is not None:
 
1748
                        parent_id_basename_delta.setdefault(
 
1749
                            new_key, [None, None])[1] = new_value
 
1750
        # validate that deletes are complete.
 
1751
        for file_id in deletes:
 
1752
            entry = self.get_entry(file_id)
 
1753
            if entry.kind != 'directory':
 
1754
                continue
 
1755
            # This loop could potentially be better by using the id_basename
 
1756
            # map to just get the child file ids.
 
1757
            for child in viewvalues(entry.children):
 
1758
                if child.file_id not in altered:
 
1759
                    raise errors.InconsistentDelta(self.id2path(child.file_id),
 
1760
                                                   child.file_id, "Child not deleted or reparented when "
 
1761
                                                   "parent deleted.")
 
1762
        result.id_to_entry.apply_delta(id_to_entry_delta)
 
1763
        if parent_id_basename_delta:
 
1764
            # Transform the parent_id_basename delta data into a linear delta
 
1765
            # with only one record for a given key. Optimally this would allow
 
1766
            # re-keying, but its simpler to just output that as a delete+add
 
1767
            # to spend less time calculating the delta.
 
1768
            delta_list = []
 
1769
            for key, (old_key, value) in viewitems(parent_id_basename_delta):
 
1770
                if value is not None:
 
1771
                    delta_list.append((old_key, key, value))
 
1772
                else:
 
1773
                    delta_list.append((old_key, None, None))
 
1774
            result.parent_id_basename_to_file_id.apply_delta(delta_list)
 
1775
        parents.discard(('', None))
 
1776
        for parent_path, parent in parents:
 
1777
            try:
 
1778
                if result.get_entry(parent).kind != 'directory':
 
1779
                    raise errors.InconsistentDelta(result.id2path(parent), parent,
 
1780
                                                   'Not a directory, but given children')
 
1781
            except errors.NoSuchId:
 
1782
                raise errors.InconsistentDelta("<unknown>", parent,
 
1783
                                               "Parent is not present in resulting inventory.")
 
1784
            if result.path2id(parent_path) != parent:
 
1785
                raise errors.InconsistentDelta(parent_path, parent,
 
1786
                                               "Parent has wrong path %r." % result.path2id(parent_path))
 
1787
        return result
 
1788
 
 
1789
    @classmethod
 
1790
    def deserialise(klass, chk_store, bytes, expected_revision_id):
 
1791
        """Deserialise a CHKInventory.
 
1792
 
 
1793
        :param chk_store: A CHK capable VersionedFiles instance.
 
1794
        :param bytes: The serialised bytes.
 
1795
        :param expected_revision_id: The revision ID we think this inventory is
 
1796
            for.
 
1797
        :return: A CHKInventory
 
1798
        """
 
1799
        lines = bytes.split(b'\n')
 
1800
        if lines[-1] != b'':
 
1801
            raise AssertionError('bytes to deserialize must end with an eol')
 
1802
        lines.pop()
 
1803
        if lines[0] != b'chkinventory:':
 
1804
            raise ValueError("not a serialised CHKInventory: %r" % bytes)
 
1805
        info = {}
 
1806
        allowed_keys = frozenset((b'root_id', b'revision_id',
 
1807
                                  b'parent_id_basename_to_file_id',
 
1808
                                  b'search_key_name', b'id_to_entry'))
 
1809
        for line in lines[1:]:
 
1810
            key, value = line.split(b': ', 1)
 
1811
            if key not in allowed_keys:
 
1812
                raise errors.BzrError('Unknown key in inventory: %r\n%r'
 
1813
                                      % (key, bytes))
 
1814
            if key in info:
 
1815
                raise errors.BzrError('Duplicate key in inventory: %r\n%r'
 
1816
                                      % (key, bytes))
 
1817
            info[key] = value
 
1818
        revision_id = bytesintern(info[b'revision_id'])
 
1819
        root_id = bytesintern(info[b'root_id'])
 
1820
        search_key_name = bytesintern(info.get(b'search_key_name', b'plain'))
 
1821
        parent_id_basename_to_file_id = bytesintern(info.get(
 
1822
            b'parent_id_basename_to_file_id', None))
 
1823
        if not parent_id_basename_to_file_id.startswith(b'sha1:'):
 
1824
            raise ValueError('parent_id_basename_to_file_id should be a sha1'
 
1825
                             ' key not %r' % (parent_id_basename_to_file_id,))
 
1826
        id_to_entry = info[b'id_to_entry']
 
1827
        if not id_to_entry.startswith(b'sha1:'):
 
1828
            raise ValueError('id_to_entry should be a sha1'
 
1829
                             ' key not %r' % (id_to_entry,))
 
1830
 
 
1831
        result = CHKInventory(search_key_name)
 
1832
        result.revision_id = revision_id
 
1833
        result.root_id = root_id
 
1834
        search_key_func = chk_map.search_key_registry.get(
 
1835
            result._search_key_name)
 
1836
        if parent_id_basename_to_file_id is not None:
 
1837
            result.parent_id_basename_to_file_id = chk_map.CHKMap(
 
1838
                chk_store, StaticTuple(parent_id_basename_to_file_id,),
 
1839
                search_key_func=search_key_func)
 
1840
        else:
 
1841
            result.parent_id_basename_to_file_id = None
 
1842
 
 
1843
        result.id_to_entry = chk_map.CHKMap(chk_store,
 
1844
                                            StaticTuple(id_to_entry,),
 
1845
                                            search_key_func=search_key_func)
 
1846
        if (result.revision_id,) != expected_revision_id:
 
1847
            raise ValueError("Mismatched revision id and expected: %r, %r" %
 
1848
                             (result.revision_id, expected_revision_id))
 
1849
        return result
 
1850
 
 
1851
    @classmethod
 
1852
    def from_inventory(klass, chk_store, inventory, maximum_size=0, search_key_name=b'plain'):
 
1853
        """Create a CHKInventory from an existing inventory.
 
1854
 
 
1855
        The content of inventory is copied into the chk_store, and a
 
1856
        CHKInventory referencing that is returned.
 
1857
 
 
1858
        :param chk_store: A CHK capable VersionedFiles instance.
 
1859
        :param inventory: The inventory to copy.
 
1860
        :param maximum_size: The CHKMap node size limit.
 
1861
        :param search_key_name: The identifier for the search key function
 
1862
        """
 
1863
        result = klass(search_key_name)
 
1864
        result.revision_id = inventory.revision_id
 
1865
        result.root_id = inventory.root.file_id
 
1866
 
 
1867
        entry_to_bytes = result._entry_to_bytes
 
1868
        parent_id_basename_key = result._parent_id_basename_key
 
1869
        id_to_entry_dict = {}
 
1870
        parent_id_basename_dict = {}
 
1871
        for path, entry in inventory.iter_entries():
 
1872
            key = StaticTuple(entry.file_id,).intern()
 
1873
            id_to_entry_dict[key] = entry_to_bytes(entry)
 
1874
            p_id_key = parent_id_basename_key(entry)
 
1875
            parent_id_basename_dict[p_id_key] = entry.file_id
 
1876
 
 
1877
        result._populate_from_dicts(chk_store, id_to_entry_dict,
 
1878
                                    parent_id_basename_dict, maximum_size=maximum_size)
 
1879
        return result
 
1880
 
 
1881
    def _populate_from_dicts(self, chk_store, id_to_entry_dict,
 
1882
                             parent_id_basename_dict, maximum_size):
 
1883
        search_key_func = chk_map.search_key_registry.get(
 
1884
            self._search_key_name)
 
1885
        root_key = chk_map.CHKMap.from_dict(chk_store, id_to_entry_dict,
 
1886
                                            maximum_size=maximum_size, key_width=1,
 
1887
                                            search_key_func=search_key_func)
 
1888
        self.id_to_entry = chk_map.CHKMap(chk_store, root_key,
 
1889
                                          search_key_func)
 
1890
        root_key = chk_map.CHKMap.from_dict(chk_store,
 
1891
                                            parent_id_basename_dict,
 
1892
                                            maximum_size=maximum_size, key_width=2,
 
1893
                                            search_key_func=search_key_func)
 
1894
        self.parent_id_basename_to_file_id = chk_map.CHKMap(chk_store,
 
1895
                                                            root_key, search_key_func)
 
1896
 
 
1897
    def _parent_id_basename_key(self, entry):
 
1898
        """Create a key for a entry in a parent_id_basename_to_file_id index."""
 
1899
        if entry.parent_id is not None:
 
1900
            parent_id = entry.parent_id
 
1901
        else:
 
1902
            parent_id = b''
 
1903
        return StaticTuple(parent_id, entry.name.encode('utf8')).intern()
 
1904
 
 
1905
    def get_entry(self, file_id):
 
1906
        """map a single file_id -> InventoryEntry."""
 
1907
        if file_id is None:
 
1908
            raise errors.NoSuchId(self, file_id)
 
1909
        result = self._fileid_to_entry_cache.get(file_id, None)
 
1910
        if result is not None:
 
1911
            return result
 
1912
        try:
 
1913
            return self._bytes_to_entry(
 
1914
                next(self.id_to_entry.iteritems([StaticTuple(file_id,)]))[1])
 
1915
        except StopIteration:
 
1916
            # really we're passing an inventory, not a tree...
 
1917
            raise errors.NoSuchId(self, file_id)
 
1918
 
 
1919
    def _getitems(self, file_ids):
 
1920
        """Similar to get_entry, but lets you query for multiple.
 
1921
 
 
1922
        The returned order is undefined. And currently if an item doesn't
 
1923
        exist, it isn't included in the output.
 
1924
        """
 
1925
        result = []
 
1926
        remaining = []
 
1927
        for file_id in file_ids:
 
1928
            entry = self._fileid_to_entry_cache.get(file_id, None)
 
1929
            if entry is None:
 
1930
                remaining.append(file_id)
 
1931
            else:
 
1932
                result.append(entry)
 
1933
        file_keys = [StaticTuple(f,).intern() for f in remaining]
 
1934
        for file_key, value in self.id_to_entry.iteritems(file_keys):
 
1935
            entry = self._bytes_to_entry(value)
 
1936
            result.append(entry)
 
1937
            self._fileid_to_entry_cache[entry.file_id] = entry
 
1938
        return result
 
1939
 
 
1940
    def has_id(self, file_id):
 
1941
        # Perhaps have an explicit 'contains' method on CHKMap ?
 
1942
        if self._fileid_to_entry_cache.get(file_id, None) is not None:
 
1943
            return True
 
1944
        return len(list(
 
1945
            self.id_to_entry.iteritems([StaticTuple(file_id,)]))) == 1
 
1946
 
 
1947
    def is_root(self, file_id):
 
1948
        return file_id == self.root_id
 
1949
 
 
1950
    def _iter_file_id_parents(self, file_id):
 
1951
        """Yield the parents of file_id up to the root."""
 
1952
        while file_id is not None:
 
1953
            try:
 
1954
                ie = self.get_entry(file_id)
 
1955
            except KeyError:
 
1956
                raise errors.NoSuchId(tree=self, file_id=file_id)
 
1957
            yield ie
 
1958
            file_id = ie.parent_id
 
1959
 
 
1960
    def iter_all_ids(self):
 
1961
        """Iterate over all file-ids."""
 
1962
        for key, _ in self.id_to_entry.iteritems():
 
1963
            yield key[-1]
 
1964
 
 
1965
    def iter_just_entries(self):
 
1966
        """Iterate over all entries.
 
1967
 
 
1968
        Unlike iter_entries(), just the entries are returned (not (path, ie))
 
1969
        and the order of entries is undefined.
 
1970
 
 
1971
        XXX: We may not want to merge this into bzr.dev.
 
1972
        """
 
1973
        for key, entry in self.id_to_entry.iteritems():
 
1974
            file_id = key[0]
 
1975
            ie = self._fileid_to_entry_cache.get(file_id, None)
 
1976
            if ie is None:
 
1977
                ie = self._bytes_to_entry(entry)
 
1978
                self._fileid_to_entry_cache[file_id] = ie
 
1979
            yield ie
 
1980
 
 
1981
    def _preload_cache(self):
 
1982
        """Make sure all file-ids are in _fileid_to_entry_cache"""
 
1983
        if self._fully_cached:
 
1984
            return  # No need to do it again
 
1985
        # The optimal sort order is to use iteritems() directly
 
1986
        cache = self._fileid_to_entry_cache
 
1987
        for key, entry in self.id_to_entry.iteritems():
 
1988
            file_id = key[0]
 
1989
            if file_id not in cache:
 
1990
                ie = self._bytes_to_entry(entry)
 
1991
                cache[file_id] = ie
 
1992
            else:
 
1993
                ie = cache[file_id]
 
1994
        last_parent_id = last_parent_ie = None
 
1995
        pid_items = self.parent_id_basename_to_file_id.iteritems()
 
1996
        for key, child_file_id in pid_items:
 
1997
            if key == (b'', b''):  # This is the root
 
1998
                if child_file_id != self.root_id:
 
1999
                    raise ValueError('Data inconsistency detected.'
 
2000
                                     ' We expected data with key ("","") to match'
 
2001
                                     ' the root id, but %s != %s'
 
2002
                                     % (child_file_id, self.root_id))
 
2003
                continue
 
2004
            parent_id, basename = key
 
2005
            ie = cache[child_file_id]
 
2006
            if parent_id == last_parent_id:
 
2007
                parent_ie = last_parent_ie
 
2008
            else:
 
2009
                parent_ie = cache[parent_id]
 
2010
            if parent_ie.kind != 'directory':
 
2011
                raise ValueError('Data inconsistency detected.'
 
2012
                                 ' An entry in the parent_id_basename_to_file_id map'
 
2013
                                 ' has parent_id {%s} but the kind of that object'
 
2014
                                 ' is %r not "directory"' % (parent_id, parent_ie.kind))
 
2015
            if parent_ie._children is None:
 
2016
                parent_ie._children = {}
 
2017
            basename = basename.decode('utf-8')
 
2018
            if basename in parent_ie._children:
 
2019
                existing_ie = parent_ie._children[basename]
 
2020
                if existing_ie != ie:
 
2021
                    raise ValueError('Data inconsistency detected.'
 
2022
                                     ' Two entries with basename %r were found'
 
2023
                                     ' in the parent entry {%s}'
 
2024
                                     % (basename, parent_id))
 
2025
            if basename != ie.name:
 
2026
                raise ValueError('Data inconsistency detected.'
 
2027
                                 ' In the parent_id_basename_to_file_id map, file_id'
 
2028
                                 ' {%s} is listed as having basename %r, but in the'
 
2029
                                 ' id_to_entry map it is %r'
 
2030
                                 % (child_file_id, basename, ie.name))
 
2031
            parent_ie._children[basename] = ie
 
2032
        self._fully_cached = True
 
2033
 
 
2034
    def iter_changes(self, basis):
 
2035
        """Generate a Tree.iter_changes change list between this and basis.
 
2036
 
 
2037
        :param basis: Another CHKInventory.
 
2038
        :return: An iterator over the changes between self and basis, as per
 
2039
            tree.iter_changes().
 
2040
        """
 
2041
        # We want: (file_id, (path_in_source, path_in_target),
 
2042
        # changed_content, versioned, parent, name, kind,
 
2043
        # executable)
 
2044
        for key, basis_value, self_value in \
 
2045
                self.id_to_entry.iter_changes(basis.id_to_entry):
 
2046
            file_id = key[0]
 
2047
            if basis_value is not None:
 
2048
                basis_entry = basis._bytes_to_entry(basis_value)
 
2049
                path_in_source = basis.id2path(file_id)
 
2050
                basis_parent = basis_entry.parent_id
 
2051
                basis_name = basis_entry.name
 
2052
                basis_executable = basis_entry.executable
 
2053
            else:
 
2054
                path_in_source = None
 
2055
                basis_parent = None
 
2056
                basis_name = None
 
2057
                basis_executable = None
 
2058
            if self_value is not None:
 
2059
                self_entry = self._bytes_to_entry(self_value)
 
2060
                path_in_target = self.id2path(file_id)
 
2061
                self_parent = self_entry.parent_id
 
2062
                self_name = self_entry.name
 
2063
                self_executable = self_entry.executable
 
2064
            else:
 
2065
                path_in_target = None
 
2066
                self_parent = None
 
2067
                self_name = None
 
2068
                self_executable = None
 
2069
            if basis_value is None:
 
2070
                # add
 
2071
                kind = (None, self_entry.kind)
 
2072
                versioned = (False, True)
 
2073
            elif self_value is None:
 
2074
                # delete
 
2075
                kind = (basis_entry.kind, None)
 
2076
                versioned = (True, False)
 
2077
            else:
 
2078
                kind = (basis_entry.kind, self_entry.kind)
 
2079
                versioned = (True, True)
 
2080
            changed_content = False
 
2081
            if kind[0] != kind[1]:
 
2082
                changed_content = True
 
2083
            elif kind[0] == 'file':
 
2084
                if (self_entry.text_size != basis_entry.text_size
 
2085
                        or self_entry.text_sha1 != basis_entry.text_sha1):
 
2086
                    changed_content = True
 
2087
            elif kind[0] == 'symlink':
 
2088
                if self_entry.symlink_target != basis_entry.symlink_target:
 
2089
                    changed_content = True
 
2090
            elif kind[0] == 'tree-reference':
 
2091
                if (self_entry.reference_revision
 
2092
                        != basis_entry.reference_revision):
 
2093
                    changed_content = True
 
2094
            parent = (basis_parent, self_parent)
 
2095
            name = (basis_name, self_name)
 
2096
            executable = (basis_executable, self_executable)
 
2097
            if (not changed_content and
 
2098
                parent[0] == parent[1] and
 
2099
                name[0] == name[1] and
 
2100
                    executable[0] == executable[1]):
 
2101
                # Could happen when only the revision changed for a directory
 
2102
                # for instance.
 
2103
                continue
 
2104
            yield (file_id, (path_in_source, path_in_target), changed_content,
 
2105
                   versioned, parent, name, kind, executable)
 
2106
 
 
2107
    def __len__(self):
 
2108
        """Return the number of entries in the inventory."""
 
2109
        return len(self.id_to_entry)
 
2110
 
 
2111
    def _make_delta(self, old):
 
2112
        """Make an inventory delta from two inventories."""
 
2113
        if not isinstance(old, CHKInventory):
 
2114
            return CommonInventory._make_delta(self, old)
 
2115
        delta = []
 
2116
        for key, old_value, self_value in \
 
2117
                self.id_to_entry.iter_changes(old.id_to_entry):
 
2118
            file_id = key[0]
 
2119
            if old_value is not None:
 
2120
                old_path = old.id2path(file_id)
 
2121
            else:
 
2122
                old_path = None
 
2123
            if self_value is not None:
 
2124
                entry = self._bytes_to_entry(self_value)
 
2125
                self._fileid_to_entry_cache[file_id] = entry
 
2126
                new_path = self.id2path(file_id)
 
2127
            else:
 
2128
                entry = None
 
2129
                new_path = None
 
2130
            delta.append((old_path, new_path, file_id, entry))
 
2131
        return delta
 
2132
 
 
2133
    def path2id(self, relpath):
 
2134
        """See CommonInventory.path2id()."""
 
2135
        # TODO: perhaps support negative hits?
 
2136
        if isinstance(relpath, (str, text_type)):
 
2137
            names = osutils.splitpath(relpath)
 
2138
        else:
 
2139
            names = relpath
 
2140
            if relpath == []:
 
2141
                relpath = [""]
 
2142
            relpath = osutils.pathjoin(*relpath)
 
2143
        result = self._path_to_fileid_cache.get(relpath, None)
 
2144
        if result is not None:
 
2145
            return result
 
2146
        current_id = self.root_id
 
2147
        if current_id is None:
 
2148
            return None
 
2149
        parent_id_index = self.parent_id_basename_to_file_id
 
2150
        cur_path = None
 
2151
        for basename in names:
 
2152
            if cur_path is None:
 
2153
                cur_path = basename
 
2154
            else:
 
2155
                cur_path = cur_path + '/' + basename
 
2156
            basename_utf8 = basename.encode('utf8')
 
2157
            file_id = self._path_to_fileid_cache.get(cur_path, None)
 
2158
            if file_id is None:
 
2159
                key_filter = [StaticTuple(current_id, basename_utf8)]
 
2160
                items = parent_id_index.iteritems(key_filter)
 
2161
                for (parent_id, name_utf8), file_id in items:
 
2162
                    if parent_id != current_id or name_utf8 != basename_utf8:
 
2163
                        raise errors.BzrError("corrupt inventory lookup! "
 
2164
                                              "%r %r %r %r" % (parent_id, current_id, name_utf8,
 
2165
                                                               basename_utf8))
 
2166
                if file_id is None:
 
2167
                    return None
 
2168
                else:
 
2169
                    self._path_to_fileid_cache[cur_path] = file_id
 
2170
            current_id = file_id
 
2171
        return current_id
 
2172
 
 
2173
    def to_lines(self):
 
2174
        """Serialise the inventory to lines."""
 
2175
        lines = [b"chkinventory:\n"]
 
2176
        if self._search_key_name != b'plain':
 
2177
            # custom ordering grouping things that don't change together
 
2178
            lines.append(b'search_key_name: %s\n' % (
 
2179
                self._search_key_name))
 
2180
            lines.append(b"root_id: %s\n" % self.root_id)
 
2181
            lines.append(b'parent_id_basename_to_file_id: %s\n' %
 
2182
                         (self.parent_id_basename_to_file_id.key()[0],))
 
2183
            lines.append(b"revision_id: %s\n" % self.revision_id)
 
2184
            lines.append(b"id_to_entry: %s\n" % (self.id_to_entry.key()[0],))
 
2185
        else:
 
2186
            lines.append(b"revision_id: %s\n" % self.revision_id)
 
2187
            lines.append(b"root_id: %s\n" % self.root_id)
 
2188
            if self.parent_id_basename_to_file_id is not None:
 
2189
                lines.append(b'parent_id_basename_to_file_id: %s\n' %
 
2190
                             (self.parent_id_basename_to_file_id.key()[0],))
 
2191
            lines.append(b"id_to_entry: %s\n" % (self.id_to_entry.key()[0],))
 
2192
        return lines
 
2193
 
 
2194
    @property
 
2195
    def root(self):
 
2196
        """Get the root entry."""
 
2197
        return self.get_entry(self.root_id)
 
2198
 
 
2199
 
 
2200
class CHKInventoryDirectory(InventoryDirectory):
 
2201
    """A directory in an inventory."""
 
2202
 
 
2203
    __slots__ = ['_children', '_chk_inventory']
 
2204
 
 
2205
    def __init__(self, file_id, name, parent_id, chk_inventory):
 
2206
        # Don't call InventoryDirectory.__init__ - it isn't right for this
 
2207
        # class.
 
2208
        InventoryEntry.__init__(self, file_id, name, parent_id)
 
2209
        self._children = None
 
2210
        self._chk_inventory = chk_inventory
 
2211
 
 
2212
    @property
 
2213
    def children(self):
 
2214
        """Access the list of children of this directory.
 
2215
 
 
2216
        With a parent_id_basename_to_file_id index, loads all the children,
 
2217
        without loads the entire index. Without is bad. A more sophisticated
 
2218
        proxy object might be nice, to allow partial loading of children as
 
2219
        well when specific names are accessed. (So path traversal can be
 
2220
        written in the obvious way but not examine siblings.).
 
2221
        """
 
2222
        if self._children is not None:
 
2223
            return self._children
 
2224
        # No longer supported
 
2225
        if self._chk_inventory.parent_id_basename_to_file_id is None:
 
2226
            raise AssertionError("Inventories without"
 
2227
                                 " parent_id_basename_to_file_id are no longer supported")
 
2228
        result = {}
 
2229
        # XXX: Todo - use proxy objects for the children rather than loading
 
2230
        # all when the attribute is referenced.
 
2231
        parent_id_index = self._chk_inventory.parent_id_basename_to_file_id
 
2232
        child_keys = set()
 
2233
        for (parent_id, name_utf8), file_id in parent_id_index.iteritems(
 
2234
                key_filter=[StaticTuple(self.file_id,)]):
 
2235
            child_keys.add(StaticTuple(file_id,))
 
2236
        cached = set()
 
2237
        for file_id_key in child_keys:
 
2238
            entry = self._chk_inventory._fileid_to_entry_cache.get(
 
2239
                file_id_key[0], None)
 
2240
            if entry is not None:
 
2241
                result[entry.name] = entry
 
2242
                cached.add(file_id_key)
 
2243
        child_keys.difference_update(cached)
 
2244
        # populate; todo: do by name
 
2245
        id_to_entry = self._chk_inventory.id_to_entry
 
2246
        for file_id_key, bytes in id_to_entry.iteritems(child_keys):
 
2247
            entry = self._chk_inventory._bytes_to_entry(bytes)
 
2248
            result[entry.name] = entry
 
2249
            self._chk_inventory._fileid_to_entry_cache[file_id_key[0]] = entry
 
2250
        self._children = result
 
2251
        return result
 
2252
 
 
2253
 
 
2254
entry_factory = {
 
2255
    'directory': InventoryDirectory,
 
2256
    'file': InventoryFile,
 
2257
    'symlink': InventoryLink,
 
2258
    'tree-reference': TreeReference
 
2259
}
 
2260
 
 
2261
 
 
2262
def make_entry(kind, name, parent_id, file_id=None):
 
2263
    """Create an inventory entry.
 
2264
 
 
2265
    :param kind: the type of inventory entry to create.
 
2266
    :param name: the basename of the entry.
 
2267
    :param parent_id: the parent_id of the entry.
 
2268
    :param file_id: the file_id to use. if None, one will be created.
 
2269
    """
 
2270
    if file_id is None:
 
2271
        file_id = generate_ids.gen_file_id(name)
 
2272
    name = ensure_normalized_name(name)
 
2273
    try:
 
2274
        factory = entry_factory[kind]
 
2275
    except KeyError:
 
2276
        raise errors.BadFileKindError(name, kind)
 
2277
    return factory(file_id, name, parent_id)
 
2278
 
 
2279
 
 
2280
def ensure_normalized_name(name):
 
2281
    """Normalize name.
 
2282
 
 
2283
    :raises InvalidNormalization: When name is not normalized, and cannot be
 
2284
        accessed on this platform by the normalized path.
 
2285
    :return: The NFC normalised version of name.
 
2286
    """
 
2287
    # ------- This has been copied to breezy.dirstate.DirState.add, please
 
2288
    # keep them synchronised.
 
2289
    # we dont import normalized_filename directly because we want to be
 
2290
    # able to change the implementation at runtime for tests.
 
2291
    norm_name, can_access = osutils.normalized_filename(name)
 
2292
    if norm_name != name:
 
2293
        if can_access:
 
2294
            return norm_name
 
2295
        else:
 
2296
            # TODO: jam 20060701 This would probably be more useful
 
2297
            #       if the error was raised with the full path
 
2298
            raise errors.InvalidNormalization(name)
 
2299
    return name
 
2300
 
 
2301
 
 
2302
_NAME_RE = lazy_regex.lazy_compile(r'^[^/\\]+$')
 
2303
 
 
2304
 
 
2305
def is_valid_name(name):
 
2306
    return bool(_NAME_RE.match(name))
 
2307
 
 
2308
 
 
2309
def _check_delta_unique_ids(delta):
 
2310
    """Decorate a delta and check that the file ids in it are unique.
 
2311
 
 
2312
    :return: A generator over delta.
 
2313
    """
 
2314
    ids = set()
 
2315
    for item in delta:
 
2316
        length = len(ids) + 1
 
2317
        ids.add(item[2])
 
2318
        if len(ids) != length:
 
2319
            raise errors.InconsistentDelta(item[0] or item[1], item[2],
 
2320
                                           "repeated file_id")
 
2321
        yield item
 
2322
 
 
2323
 
 
2324
def _check_delta_unique_new_paths(delta):
 
2325
    """Decorate a delta and check that the new paths in it are unique.
 
2326
 
 
2327
    :return: A generator over delta.
 
2328
    """
 
2329
    paths = set()
 
2330
    for item in delta:
 
2331
        length = len(paths) + 1
 
2332
        path = item[1]
 
2333
        if path is not None:
 
2334
            paths.add(path)
 
2335
            if len(paths) != length:
 
2336
                raise errors.InconsistentDelta(path, item[2], "repeated path")
 
2337
        yield item
 
2338
 
 
2339
 
 
2340
def _check_delta_unique_old_paths(delta):
 
2341
    """Decorate a delta and check that the old paths in it are unique.
 
2342
 
 
2343
    :return: A generator over delta.
 
2344
    """
 
2345
    paths = set()
 
2346
    for item in delta:
 
2347
        length = len(paths) + 1
 
2348
        path = item[0]
 
2349
        if path is not None:
 
2350
            paths.add(path)
 
2351
            if len(paths) != length:
 
2352
                raise errors.InconsistentDelta(path, item[2], "repeated path")
 
2353
        yield item
 
2354
 
 
2355
 
 
2356
def _check_delta_ids_are_valid(delta):
 
2357
    """Decorate a delta and check that the ids in it are valid.
 
2358
 
 
2359
    :return: A generator over delta.
 
2360
    """
 
2361
    for item in delta:
 
2362
        entry = item[3]
 
2363
        if item[2] is None:
 
2364
            raise errors.InconsistentDelta(item[0] or item[1], item[2],
 
2365
                                           "entry with file_id None %r" % entry)
 
2366
        if not isinstance(item[2], bytes):
 
2367
            raise errors.InconsistentDelta(item[0] or item[1], item[2],
 
2368
                                           "entry with non bytes file_id %r" % entry)
 
2369
        yield item
 
2370
 
 
2371
 
 
2372
def _check_delta_ids_match_entry(delta):
 
2373
    """Decorate a delta and check that the ids in it match the entry.file_id.
 
2374
 
 
2375
    :return: A generator over delta.
 
2376
    """
 
2377
    for item in delta:
 
2378
        entry = item[3]
 
2379
        if entry is not None:
 
2380
            if entry.file_id != item[2]:
 
2381
                raise errors.InconsistentDelta(item[0] or item[1], item[2],
 
2382
                                               "mismatched id with %r" % entry)
 
2383
        yield item
 
2384
 
 
2385
 
 
2386
def _check_delta_new_path_entry_both_or_None(delta):
 
2387
    """Decorate a delta and check that the new_path and entry are paired.
 
2388
 
 
2389
    :return: A generator over delta.
 
2390
    """
 
2391
    for item in delta:
 
2392
        new_path = item[1]
 
2393
        entry = item[3]
 
2394
        if new_path is None and entry is not None:
 
2395
            raise errors.InconsistentDelta(item[0], item[1],
 
2396
                                           "Entry with no new_path")
 
2397
        if new_path is not None and entry is None:
 
2398
            raise errors.InconsistentDelta(new_path, item[1],
 
2399
                                           "new_path with no entry")
 
2400
        yield item
 
2401
 
 
2402
 
 
2403
def mutable_inventory_from_tree(tree):
 
2404
    """Create a new inventory that has the same contents as a specified tree.
 
2405
 
 
2406
    :param tree: Revision tree to create inventory from
 
2407
    """
 
2408
    entries = tree.iter_entries_by_dir()
 
2409
    inv = Inventory(None, tree.get_revision_id())
 
2410
    for path, inv_entry in entries:
 
2411
        inv.add(inv_entry.copy())
 
2412
    return inv