~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/bundle/serializer/v4.py

  • Committer: Patch Queue Manager
  • Date: 2015-12-17 18:39:00 UTC
  • mfrom: (6606.1.2 fix-float)
  • Revision ID: pqm@pqm.ubuntu.com-20151217183900-0719du2uv1kwu3lc
(vila) Inline testtools private method to fix an issue in xenial (the
 private implementation has changed in an backward incompatible way).
 (Jelmer Vernooij)

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2007 Canonical Ltd
 
1
# Copyright (C) 2007-2010 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
12
12
#
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
 
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
16
 
 
17
from __future__ import absolute_import
16
18
 
17
19
from cStringIO import StringIO
18
20
import bz2
19
21
import re
20
22
 
21
23
from bzrlib import (
22
 
    diff,
23
24
    errors,
24
25
    iterablefile,
 
26
    lru_cache,
25
27
    multiparent,
26
28
    osutils,
27
29
    pack,
28
30
    revision as _mod_revision,
 
31
    serializer,
29
32
    trace,
30
 
    xml_serializer,
 
33
    ui,
 
34
    versionedfile as _mod_versionedfile,
31
35
    )
32
 
from bzrlib.bundle import bundle_data, serializer
33
 
from bzrlib.util import bencode
 
36
from bzrlib.bundle import bundle_data, serializer as bundle_serializer
 
37
from bzrlib.i18n import ngettext
 
38
from bzrlib import bencode
 
39
 
 
40
 
 
41
class _MPDiffInventoryGenerator(_mod_versionedfile._MPDiffGenerator):
 
42
    """Generate Inventory diffs serialized inventories."""
 
43
 
 
44
    def __init__(self, repo, inventory_keys):
 
45
        super(_MPDiffInventoryGenerator, self).__init__(repo.inventories,
 
46
            inventory_keys)
 
47
        self.repo = repo
 
48
        self.sha1s = {}
 
49
 
 
50
    def iter_diffs(self):
 
51
        """Compute the diffs one at a time."""
 
52
        # This is instead of compute_diffs() since we guarantee our ordering of
 
53
        # inventories, we don't have to do any buffering
 
54
        self._find_needed_keys()
 
55
        # We actually use a slightly different ordering. We grab all of the
 
56
        # parents first, and then grab the ordered requests.
 
57
        needed_ids = [k[-1] for k in self.present_parents]
 
58
        needed_ids.extend([k[-1] for k in self.ordered_keys])
 
59
        inv_to_str = self.repo._serializer.write_inventory_to_string
 
60
        for inv in self.repo.iter_inventories(needed_ids):
 
61
            revision_id = inv.revision_id
 
62
            key = (revision_id,)
 
63
            if key in self.present_parents:
 
64
                # Not a key we will transmit, which is a shame, since because
 
65
                # of that bundles don't work with stacked branches
 
66
                parent_ids = None
 
67
            else:
 
68
                parent_ids = [k[-1] for k in self.parent_map[key]]
 
69
            as_bytes = inv_to_str(inv)
 
70
            self._process_one_record(key, (as_bytes,))
 
71
            if parent_ids is None:
 
72
                continue
 
73
            diff = self.diffs.pop(key)
 
74
            sha1 = osutils.sha_string(as_bytes)
 
75
            yield revision_id, parent_ids, sha1, diff
34
76
 
35
77
 
36
78
class BundleWriter(object):
54
96
 
55
97
    def begin(self):
56
98
        """Start writing the bundle"""
57
 
        self._fileobj.write(serializer._get_bundle_header(
58
 
            serializer.v4_string))
 
99
        self._fileobj.write(bundle_serializer._get_bundle_header(
 
100
            bundle_serializer.v4_string))
59
101
        self._fileobj.write('#\n')
60
102
        self._container.begin()
61
103
 
107
149
    @staticmethod
108
150
    def encode_name(content_kind, revision_id, file_id=None):
109
151
        """Encode semantic ids as a container name"""
110
 
        assert content_kind in ('revision', 'file', 'inventory', 'signature',
111
 
                                'info')
112
 
 
 
152
        if content_kind not in ('revision', 'file', 'inventory', 'signature',
 
153
                'info'):
 
154
            raise ValueError(content_kind)
113
155
        if content_kind == 'file':
114
 
            assert file_id is not None
 
156
            if file_id is None:
 
157
                raise AssertionError()
115
158
        else:
116
 
            assert file_id is None
 
159
            if file_id is not None:
 
160
                raise AssertionError()
117
161
        if content_kind == 'info':
118
 
            assert revision_id is None
119
 
        else:
120
 
            assert revision_id is not None
 
162
            if revision_id is not None:
 
163
                raise AssertionError()
 
164
        elif revision_id is None:
 
165
            raise AssertionError()
121
166
        names = [n.replace('/', '//') for n in
122
167
                 (content_kind, revision_id, file_id) if n is not None]
123
168
        return '/'.join(names)
131
176
        """
132
177
        name = self.encode_name(repo_kind, revision_id, file_id)
133
178
        encoded_metadata = bencode.bencode(metadata)
134
 
        self._container.add_bytes_record(encoded_metadata, [name])
 
179
        self._container.add_bytes_record(encoded_metadata, [(name, )])
135
180
        if metadata['storage_kind'] != 'header':
136
181
            self._container.add_bytes_record(bytes, [])
137
182
 
144
189
    body
145
190
    """
146
191
 
147
 
    def __init__(self, fileobj):
 
192
    def __init__(self, fileobj, stream_input=True):
 
193
        """Constructor
 
194
 
 
195
        :param fileobj: a file containing a bzip-encoded container
 
196
        :param stream_input: If True, the BundleReader stream input rather than
 
197
            reading it all into memory at once.  Reading it into memory all at
 
198
            once is (currently) faster.
 
199
        """
148
200
        line = fileobj.readline()
149
201
        if line != '\n':
150
202
            fileobj.readline()
151
203
        self.patch_lines = []
152
 
        self._container = pack.ContainerReader(
153
 
            iterablefile.IterableFile(self.iter_decode(fileobj)))
 
204
        if stream_input:
 
205
            source_file = iterablefile.IterableFile(self.iter_decode(fileobj))
 
206
        else:
 
207
            source_file = StringIO(bz2.decompress(fileobj.read()))
 
208
        self._container_file = source_file
154
209
 
155
210
    @staticmethod
156
211
    def iter_decode(fileobj):
157
212
        """Iterate through decoded fragments of the file"""
158
213
        decompressor = bz2.BZ2Decompressor()
159
214
        for line in fileobj:
160
 
            yield decompressor.decompress(line)
 
215
            try:
 
216
                yield decompressor.decompress(line)
 
217
            except EOFError:
 
218
                return
161
219
 
162
220
    @staticmethod
163
221
    def decode_name(name):
189
247
        :return: a generator of (bytes, metadata, content_kind, revision_id,
190
248
            file_id)
191
249
        """
192
 
        iterator = self._container.iter_records()
193
 
        for names, meta_bytes in iterator:
 
250
        iterator = pack.iter_records_from_file(self._container_file)
 
251
        for names, bytes in iterator:
194
252
            if len(names) != 1:
195
253
                raise errors.BadBundle('Record has %d names instead of 1'
196
254
                                       % len(names))
197
 
            metadata = bencode.bdecode(meta_bytes(None))
 
255
            metadata = bencode.bdecode(bytes)
198
256
            if metadata['storage_kind'] == 'header':
199
257
                bytes = None
200
258
            else:
201
259
                _unused, bytes = iterator.next()
202
 
                bytes = bytes(None)
203
 
            yield (bytes, metadata) + self.decode_name(names[0])
204
 
 
205
 
 
206
 
class BundleSerializerV4(serializer.BundleSerializer):
 
260
            yield (bytes, metadata) + self.decode_name(names[0][0])
 
261
 
 
262
 
 
263
class BundleSerializerV4(bundle_serializer.BundleSerializer):
207
264
    """Implement the high-level bundle interface"""
208
265
 
209
266
    def write(self, repository, revision_ids, forced_bases, fileobj):
235
292
    @staticmethod
236
293
    def get_source_serializer(info):
237
294
        """Retrieve the serializer for a given info object"""
238
 
        return xml_serializer.format_registry.get(info['serializer'])
 
295
        return serializer.format_registry.get(info['serializer'])
239
296
 
240
297
 
241
298
class BundleWriteOperation(object):
255
312
        self.repository = repository
256
313
        bundle = BundleWriter(fileobj)
257
314
        self.bundle = bundle
258
 
        self.base_ancestry = set(repository.get_ancestry(base,
259
 
                                                         topo_sorted=False))
260
315
        if revision_ids is not None:
261
316
            self.revision_ids = revision_ids
262
317
        else:
263
 
            revision_ids = set(repository.get_ancestry(target,
264
 
                                                       topo_sorted=False))
265
 
            self.revision_ids = revision_ids.difference(self.base_ancestry)
 
318
            graph = repository.get_graph()
 
319
            revision_ids = graph.find_unique_ancestors(target, [base])
 
320
            # Strip ghosts
 
321
            parents = graph.get_parent_map(revision_ids)
 
322
            self.revision_ids = [r for r in revision_ids if r in parents]
 
323
        self.revision_keys = set([(revid,) for revid in self.revision_ids])
266
324
 
267
325
    def do_write(self):
268
326
        """Write all data to the bundle"""
269
 
        self.bundle.begin()
270
 
        self.write_info()
271
 
        self.write_files()
272
 
        self.write_revisions()
273
 
        self.bundle.end()
 
327
        trace.note(ngettext('Bundling %d revision.', 'Bundling %d revisions.',
 
328
                            len(self.revision_ids)), len(self.revision_ids))
 
329
        self.repository.lock_read()
 
330
        try:
 
331
            self.bundle.begin()
 
332
            self.write_info()
 
333
            self.write_files()
 
334
            self.write_revisions()
 
335
            self.bundle.end()
 
336
        finally:
 
337
            self.repository.unlock()
274
338
        return self.revision_ids
275
339
 
276
340
    def write_info(self):
281
345
        self.bundle.add_info_record(serializer=serializer_format,
282
346
                                    supports_rich_root=supports_rich_root)
283
347
 
284
 
    def iter_file_revisions(self):
285
 
        """Iterate through all relevant revisions of all files.
286
 
 
287
 
        This is the correct implementation, but is not compatible with bzr.dev,
288
 
        because certain old revisions were not converted correctly, and have
289
 
        the wrong "revision" marker in inventories.
290
 
        """
291
 
        transaction = self.repository.get_transaction()
292
 
        altered = self.repository.fileids_altered_by_revision_ids(
293
 
            self.revision_ids)
294
 
        for file_id, file_revision_ids in altered.iteritems():
295
 
            vf = self.repository.weave_store.get_weave(file_id, transaction)
296
 
            yield vf, file_id, file_revision_ids
297
 
 
298
 
    def iter_file_revisions_aggressive(self):
299
 
        """Iterate through all relevant revisions of all files.
300
 
 
301
 
        This uses the standard iter_file_revisions to determine what revisions
302
 
        are referred to by inventories, but then uses the versionedfile to
303
 
        determine what the build-dependencies of each required revision.
304
 
 
305
 
        All build dependencies which are not ancestors of the base revision
306
 
        are emitted.
307
 
        """
308
 
        for vf, file_id, file_revision_ids in self.iter_file_revisions():
309
 
            new_revision_ids = set()
310
 
            pending = list(file_revision_ids)
311
 
            while len(pending) > 0:
312
 
                revision_id = pending.pop()
313
 
                if revision_id in new_revision_ids:
314
 
                    continue
315
 
                if revision_id in self.base_ancestry:
316
 
                    continue
317
 
                new_revision_ids.add(revision_id)
318
 
                pending.extend(vf.get_parents(revision_id))
319
 
            yield vf, file_id, new_revision_ids
320
 
 
321
348
    def write_files(self):
322
349
        """Write bundle records for all revisions of all files"""
323
 
        for vf, file_id, revision_ids in self.iter_file_revisions_aggressive():
324
 
            self.add_mp_records('file', file_id, vf, revision_ids)
 
350
        text_keys = []
 
351
        altered_fileids = self.repository.fileids_altered_by_revision_ids(
 
352
                self.revision_ids)
 
353
        for file_id, revision_ids in altered_fileids.iteritems():
 
354
            for revision_id in revision_ids:
 
355
                text_keys.append((file_id, revision_id))
 
356
        self._add_mp_records_keys('file', self.repository.texts, text_keys)
325
357
 
326
358
    def write_revisions(self):
327
359
        """Write bundle records for all revisions and signatures"""
328
 
        inv_vf = self.repository.get_inventory_weave()
329
 
        revision_order = list(multiparent.topo_iter(inv_vf, self.revision_ids))
 
360
        inv_vf = self.repository.inventories
 
361
        topological_order = [key[-1] for key in multiparent.topo_iter_keys(
 
362
                                inv_vf, self.revision_keys)]
 
363
        revision_order = topological_order
330
364
        if self.target is not None and self.target in self.revision_ids:
 
365
            # Make sure the target revision is always the last entry
 
366
            revision_order = list(topological_order)
331
367
            revision_order.remove(self.target)
332
368
            revision_order.append(self.target)
333
 
        self.add_mp_records('inventory', None, inv_vf, revision_order)
334
 
        parents_list = self.repository.get_parents(revision_order)
335
 
        for parents, revision_id in zip(parents_list, revision_order):
336
 
            revision_text = self.repository.get_revision_xml(revision_id)
 
369
        if self.repository._serializer.support_altered_by_hack:
 
370
            # Repositories that support_altered_by_hack means that
 
371
            # inventories.make_mpdiffs() contains all the data about the tree
 
372
            # shape. Formats without support_altered_by_hack require
 
373
            # chk_bytes/etc, so we use a different code path.
 
374
            self._add_mp_records_keys('inventory', inv_vf,
 
375
                                      [(revid,) for revid in topological_order])
 
376
        else:
 
377
            # Inventories should always be added in pure-topological order, so
 
378
            # that we can apply the mpdiff for the child to the parent texts.
 
379
            self._add_inventory_mpdiffs_from_serializer(topological_order)
 
380
        self._add_revision_texts(revision_order)
 
381
 
 
382
    def _add_inventory_mpdiffs_from_serializer(self, revision_order):
 
383
        """Generate mpdiffs by serializing inventories.
 
384
 
 
385
        The current repository only has part of the tree shape information in
 
386
        the 'inventories' vf. So we use serializer.write_inventory_to_string to
 
387
        get a 'full' representation of the tree shape, and then generate
 
388
        mpdiffs on that data stream. This stream can then be reconstructed on
 
389
        the other side.
 
390
        """
 
391
        inventory_key_order = [(r,) for r in revision_order]
 
392
        generator = _MPDiffInventoryGenerator(self.repository,
 
393
                                              inventory_key_order)
 
394
        for revision_id, parent_ids, sha1, diff in generator.iter_diffs():
 
395
            text = ''.join(diff.to_patch())
 
396
            self.bundle.add_multiparent_record(text, sha1, parent_ids,
 
397
                                               'inventory', revision_id, None)
 
398
 
 
399
    def _add_revision_texts(self, revision_order):
 
400
        parent_map = self.repository.get_parent_map(revision_order)
 
401
        revision_to_str = self.repository._serializer.write_revision_to_string
 
402
        revisions = self.repository.get_revisions(revision_order)
 
403
        for revision in revisions:
 
404
            revision_id = revision.revision_id
 
405
            parents = parent_map.get(revision_id, None)
 
406
            revision_text = revision_to_str(revision)
337
407
            self.bundle.add_fulltext_record(revision_text, parents,
338
408
                                       'revision', revision_id)
339
409
            try:
358
428
                base = parents[0]
359
429
        return base, target
360
430
 
361
 
    def add_mp_records(self, repo_kind, file_id, vf, revision_ids):
 
431
    def _add_mp_records_keys(self, repo_kind, vf, keys):
362
432
        """Add multi-parent diff records to a bundle"""
363
 
        revision_ids = list(multiparent.topo_iter(vf, revision_ids))
364
 
        mpdiffs = vf.make_mpdiffs(revision_ids)
365
 
        sha1s = vf.get_sha1s(revision_ids)
366
 
        for mpdiff, revision_id, sha1, in zip(mpdiffs, revision_ids, sha1s):
367
 
            parents = vf.get_parents(revision_id)
 
433
        ordered_keys = list(multiparent.topo_iter_keys(vf, keys))
 
434
        mpdiffs = vf.make_mpdiffs(ordered_keys)
 
435
        sha1s = vf.get_sha1s(ordered_keys)
 
436
        parent_map = vf.get_parent_map(ordered_keys)
 
437
        for mpdiff, item_key, in zip(mpdiffs, ordered_keys):
 
438
            sha1 = sha1s[item_key]
 
439
            parents = [key[-1] for key in parent_map[item_key]]
368
440
            text = ''.join(mpdiff.to_patch())
 
441
            # Infer file id records as appropriate.
 
442
            if len(item_key) == 2:
 
443
                file_id = item_key[0]
 
444
            else:
 
445
                file_id = None
369
446
            self.bundle.add_multiparent_record(text, sha1, parents, repo_kind,
370
 
                                               revision_id, file_id)
 
447
                                               item_key[-1], file_id)
371
448
 
372
449
 
373
450
class BundleInfoV4(object):
382
459
    def install(self, repository):
383
460
        return self.install_revisions(repository)
384
461
 
385
 
    def install_revisions(self, repository):
386
 
        """Install this bundle's revisions into the specified repository"""
 
462
    def install_revisions(self, repository, stream_input=True):
 
463
        """Install this bundle's revisions into the specified repository
 
464
 
 
465
        :param target_repo: The repository to install into
 
466
        :param stream_input: If True, will stream input rather than reading it
 
467
            all into memory at once.  Reading it into memory all at once is
 
468
            (currently) faster.
 
469
        """
387
470
        repository.lock_write()
388
471
        try:
389
 
            ri = RevisionInstaller(self.get_bundle_reader(),
 
472
            ri = RevisionInstaller(self.get_bundle_reader(stream_input),
390
473
                                   self._serializer, repository)
391
474
            return ri.install()
392
475
        finally:
399
482
        """
400
483
        return None, self.target, 'inapplicable'
401
484
 
402
 
    def get_bundle_reader(self):
 
485
    def get_bundle_reader(self, stream_input=True):
 
486
        """Return a new BundleReader for the associated bundle
 
487
 
 
488
        :param stream_input: If True, the BundleReader stream input rather than
 
489
            reading it all into memory at once.  Reading it into memory all at
 
490
            once is (currently) faster.
 
491
        """
403
492
        self._fileobj.seek(0)
404
 
        return BundleReader(self._fileobj)
 
493
        return BundleReader(self._fileobj, stream_input)
405
494
 
406
495
    def _get_real_revisions(self):
407
496
        if self.__real_revisions is None:
444
533
        self._info = None
445
534
 
446
535
    def install(self):
447
 
        """Perform the installation"""
 
536
        """Perform the installation.
 
537
 
 
538
        Must be called with the Repository locked.
 
539
        """
 
540
        self._repository.start_write_group()
 
541
        try:
 
542
            result = self._install_in_write_group()
 
543
        except:
 
544
            self._repository.abort_write_group()
 
545
            raise
 
546
        self._repository.commit_write_group()
 
547
        return result
 
548
 
 
549
    def _install_in_write_group(self):
448
550
        current_file = None
449
551
        current_versionedfile = None
450
552
        pending_file_records = []
 
553
        inventory_vf = None
 
554
        pending_inventory_records = []
451
555
        added_inv = set()
452
556
        target_revision = None
453
557
        for bytes, metadata, repo_kind, revision_id, file_id in\
454
558
            self._container.iter_records():
455
559
            if repo_kind == 'info':
456
 
                assert self._info is None
 
560
                if self._info is not None:
 
561
                    raise AssertionError()
457
562
                self._handle_info(metadata)
458
 
            if repo_kind != 'file':
459
 
                self._install_mp_records(current_versionedfile,
 
563
            if (pending_file_records and
 
564
                (repo_kind, file_id) != ('file', current_file)):
 
565
                # Flush the data for a single file - prevents memory
 
566
                # spiking due to buffering all files in memory.
 
567
                self._install_mp_records_keys(self._repository.texts,
460
568
                    pending_file_records)
461
569
                current_file = None
462
 
                current_versionedfile = None
463
 
                pending_file_records = []
464
 
                if repo_kind == 'inventory':
465
 
                    self._install_inventory(revision_id, metadata, bytes)
466
 
                if repo_kind == 'revision':
467
 
                    target_revision = revision_id
468
 
                    self._install_revision(revision_id, metadata, bytes)
469
 
                if repo_kind == 'signature':
470
 
                    self._install_signature(revision_id, metadata, bytes)
 
570
                del pending_file_records[:]
 
571
            if len(pending_inventory_records) > 0 and repo_kind != 'inventory':
 
572
                self._install_inventory_records(pending_inventory_records)
 
573
                pending_inventory_records = []
 
574
            if repo_kind == 'inventory':
 
575
                pending_inventory_records.append(((revision_id,), metadata, bytes))
 
576
            if repo_kind == 'revision':
 
577
                target_revision = revision_id
 
578
                self._install_revision(revision_id, metadata, bytes)
 
579
            if repo_kind == 'signature':
 
580
                self._install_signature(revision_id, metadata, bytes)
471
581
            if repo_kind == 'file':
472
 
                if file_id != current_file:
473
 
                    self._install_mp_records(current_versionedfile,
474
 
                        pending_file_records)
475
 
                    current_file = file_id
476
 
                    current_versionedfile = \
477
 
                        self._repository.weave_store.get_weave_or_empty(
478
 
                        file_id, self._repository.get_transaction())
479
 
                    pending_file_records = []
480
 
                if revision_id in current_versionedfile:
481
 
                    continue
482
 
                pending_file_records.append((revision_id, metadata, bytes))
483
 
        self._install_mp_records(current_versionedfile, pending_file_records)
 
582
                current_file = file_id
 
583
                pending_file_records.append(((file_id, revision_id), metadata, bytes))
 
584
        self._install_mp_records_keys(self._repository.texts, pending_file_records)
484
585
        return target_revision
485
586
 
486
587
    def _handle_info(self, info):
501
602
                      records if r not in versionedfile]
502
603
        versionedfile.add_mpdiffs(vf_records)
503
604
 
504
 
    def _install_inventory(self, revision_id, metadata, text):
505
 
        vf = self._repository.get_inventory_weave()
506
 
        if revision_id in vf:
507
 
            return
508
 
        parent_ids = metadata['parents']
509
 
        if self._info['serializer'] == self._repository._serializer.format_num:
510
 
            return self._install_mp_records(vf, [(revision_id, metadata,
511
 
                                                  text)])
512
 
        parents = [self._repository.get_inventory(p)
513
 
                   for p in parent_ids]
514
 
        parent_texts = [self._source_serializer.write_inventory_to_string(p)
515
 
                        for p in parents]
516
 
        target_lines = multiparent.MultiParent.from_patch(text).to_lines(
517
 
            parent_texts)
518
 
        sha1 = osutils.sha_strings(target_lines)
519
 
        if sha1 != metadata['sha1']:
520
 
            raise errors.BadBundle("Can't convert to target format")
521
 
        target_inv = self._source_serializer.read_inventory_from_string(
522
 
            ''.join(target_lines))
523
 
        self._handle_root(target_inv, parent_ids)
 
605
    def _install_mp_records_keys(self, versionedfile, records):
 
606
        d_func = multiparent.MultiParent.from_patch
 
607
        vf_records = []
 
608
        for key, meta, text in records:
 
609
            # Adapt to tuple interface: A length two key is a file_id,
 
610
            # revision_id pair, a length 1 key is a
 
611
            # revision/signature/inventory. We need to do this because
 
612
            # the metadata extraction from the bundle has not yet been updated
 
613
            # to use the consistent tuple interface itself.
 
614
            if len(key) == 2:
 
615
                prefix = key[:1]
 
616
            else:
 
617
                prefix = ()
 
618
            parents = [prefix + (parent,) for parent in meta['parents']]
 
619
            vf_records.append((key, parents, meta['sha1'], d_func(text)))
 
620
        versionedfile.add_mpdiffs(vf_records)
 
621
 
 
622
    def _get_parent_inventory_texts(self, inventory_text_cache,
 
623
                                    inventory_cache, parent_ids):
 
624
        cached_parent_texts = {}
 
625
        remaining_parent_ids = []
 
626
        for parent_id in parent_ids:
 
627
            p_text = inventory_text_cache.get(parent_id, None)
 
628
            if p_text is None:
 
629
                remaining_parent_ids.append(parent_id)
 
630
            else:
 
631
                cached_parent_texts[parent_id] = p_text
 
632
        ghosts = ()
 
633
        # TODO: Use inventory_cache to grab inventories we already have in
 
634
        #       memory
 
635
        if remaining_parent_ids:
 
636
            # first determine what keys are actually present in the local
 
637
            # inventories object (don't use revisions as they haven't been
 
638
            # installed yet.)
 
639
            parent_keys = [(r,) for r in remaining_parent_ids]
 
640
            present_parent_map = self._repository.inventories.get_parent_map(
 
641
                                        parent_keys)
 
642
            present_parent_ids = []
 
643
            ghosts = set()
 
644
            for p_id in remaining_parent_ids:
 
645
                if (p_id,) in present_parent_map:
 
646
                    present_parent_ids.append(p_id)
 
647
                else:
 
648
                    ghosts.add(p_id)
 
649
            to_string = self._source_serializer.write_inventory_to_string
 
650
            for parent_inv in self._repository.iter_inventories(
 
651
                                    present_parent_ids):
 
652
                p_text = to_string(parent_inv)
 
653
                inventory_cache[parent_inv.revision_id] = parent_inv
 
654
                cached_parent_texts[parent_inv.revision_id] = p_text
 
655
                inventory_text_cache[parent_inv.revision_id] = p_text
 
656
 
 
657
        parent_texts = [cached_parent_texts[parent_id]
 
658
                        for parent_id in parent_ids
 
659
                         if parent_id not in ghosts]
 
660
        return parent_texts
 
661
 
 
662
    def _install_inventory_records(self, records):
 
663
        if (self._info['serializer'] == self._repository._serializer.format_num
 
664
            and self._repository._serializer.support_altered_by_hack):
 
665
            return self._install_mp_records_keys(self._repository.inventories,
 
666
                records)
 
667
        # Use a 10MB text cache, since these are string xml inventories. Note
 
668
        # that 10MB is fairly small for large projects (a single inventory can
 
669
        # be >5MB). Another possibility is to cache 10-20 inventory texts
 
670
        # instead
 
671
        inventory_text_cache = lru_cache.LRUSizeCache(10*1024*1024)
 
672
        # Also cache the in-memory representation. This allows us to create
 
673
        # inventory deltas to apply rather than calling add_inventory from
 
674
        # scratch each time.
 
675
        inventory_cache = lru_cache.LRUCache(10)
 
676
        pb = ui.ui_factory.nested_progress_bar()
524
677
        try:
525
 
            self._repository.add_inventory(revision_id, target_inv, parent_ids)
526
 
        except errors.UnsupportedInventoryKind:
527
 
            raise errors.IncompatibleRevision(repr(self._repository))
 
678
            num_records = len(records)
 
679
            for idx, (key, metadata, bytes) in enumerate(records):
 
680
                pb.update('installing inventory', idx, num_records)
 
681
                revision_id = key[-1]
 
682
                parent_ids = metadata['parents']
 
683
                # Note: This assumes the local ghosts are identical to the
 
684
                #       ghosts in the source, as the Bundle serialization
 
685
                #       format doesn't record ghosts.
 
686
                p_texts = self._get_parent_inventory_texts(inventory_text_cache,
 
687
                                                           inventory_cache,
 
688
                                                           parent_ids)
 
689
                # Why does to_lines() take strings as the source, it seems that
 
690
                # it would have to cast to a list of lines, which we get back
 
691
                # as lines and then cast back to a string.
 
692
                target_lines = multiparent.MultiParent.from_patch(bytes
 
693
                            ).to_lines(p_texts)
 
694
                inv_text = ''.join(target_lines)
 
695
                del target_lines
 
696
                sha1 = osutils.sha_string(inv_text)
 
697
                if sha1 != metadata['sha1']:
 
698
                    raise errors.BadBundle("Can't convert to target format")
 
699
                # Add this to the cache so we don't have to extract it again.
 
700
                inventory_text_cache[revision_id] = inv_text
 
701
                target_inv = self._source_serializer.read_inventory_from_string(
 
702
                    inv_text)
 
703
                self._handle_root(target_inv, parent_ids)
 
704
                parent_inv = None
 
705
                if parent_ids:
 
706
                    parent_inv = inventory_cache.get(parent_ids[0], None)
 
707
                try:
 
708
                    if parent_inv is None:
 
709
                        self._repository.add_inventory(revision_id, target_inv,
 
710
                                                       parent_ids)
 
711
                    else:
 
712
                        delta = target_inv._make_delta(parent_inv)
 
713
                        self._repository.add_inventory_by_delta(parent_ids[0],
 
714
                            delta, revision_id, parent_ids)
 
715
                except errors.UnsupportedInventoryKind:
 
716
                    raise errors.IncompatibleRevision(repr(self._repository))
 
717
                inventory_cache[revision_id] = target_inv
 
718
        finally:
 
719
            pb.finished()
528
720
 
529
721
    def _handle_root(self, target_inv, parent_ids):
530
722
        revision_id = target_inv.revision_id
531
723
        if self.update_root:
532
 
            target_inv.root.revision = revision_id
533
 
            store = self._repository.weave_store
534
 
            transaction = self._repository.get_transaction()
535
 
            vf = store.get_weave_or_empty(target_inv.root.file_id, transaction)
536
 
            vf.add_lines(revision_id, parent_ids, [])
 
724
            text_key = (target_inv.root.file_id, revision_id)
 
725
            parent_keys = [(target_inv.root.file_id, parent) for
 
726
                parent in parent_ids]
 
727
            self._repository.texts.add_lines(text_key, parent_keys, [])
537
728
        elif not self._repository.supports_rich_root():
538
729
            if target_inv.root.revision != revision_id:
539
730
                raise errors.IncompatibleRevision(repr(self._repository))
540
731
 
541
 
 
542
732
    def _install_revision(self, revision_id, metadata, text):
543
733
        if self._repository.has_revision(revision_id):
544
734
            return
545
 
        self._repository._add_revision_text(revision_id, text)
 
735
        revision = self._source_serializer.read_revision_from_string(text)
 
736
        self._repository.add_revision(revision.revision_id, revision)
546
737
 
547
738
    def _install_signature(self, revision_id, metadata, text):
548
739
        transaction = self._repository.get_transaction()
549
 
        if self._repository._revision_store.has_signature(revision_id,
550
 
                                                          transaction):
 
740
        if self._repository.has_signature_for_revision_id(revision_id):
551
741
            return
552
 
        self._repository._revision_store.add_revision_signature_text(
553
 
            revision_id, text, transaction)
 
742
        self._repository.add_signature_text(revision_id, text)