~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

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

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2007-03-28 06:58:22 UTC
  • mfrom: (2379.2.3 hpss-chroot)
  • Revision ID: pqm@pqm.ubuntu.com-20070328065822-999550a858a3ced3
(robertc) Fix chroot urls to not expose the url of the transport they are protecting, allowing regular url operations to work on them. (Robert Collins, Andrew Bennetts)

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2007-2010 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
 
from __future__ import absolute_import
18
 
 
19
 
from cStringIO import StringIO
20
 
import bz2
21
 
import re
22
 
 
23
 
from bzrlib import (
24
 
    errors,
25
 
    iterablefile,
26
 
    lru_cache,
27
 
    multiparent,
28
 
    osutils,
29
 
    pack,
30
 
    revision as _mod_revision,
31
 
    serializer,
32
 
    trace,
33
 
    ui,
34
 
    versionedfile as _mod_versionedfile,
35
 
    )
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
76
 
 
77
 
 
78
 
class BundleWriter(object):
79
 
    """Writer for bundle-format files.
80
 
 
81
 
    This serves roughly the same purpose as ContainerReader, but acts as a
82
 
    layer on top of it.
83
 
 
84
 
    Provides ways of writing the specific record types supported this bundle
85
 
    format.
86
 
    """
87
 
 
88
 
    def __init__(self, fileobj):
89
 
        self._container = pack.ContainerWriter(self._write_encoded)
90
 
        self._fileobj = fileobj
91
 
        self._compressor = bz2.BZ2Compressor()
92
 
 
93
 
    def _write_encoded(self, bytes):
94
 
        """Write bzip2-encoded bytes to the file"""
95
 
        self._fileobj.write(self._compressor.compress(bytes))
96
 
 
97
 
    def begin(self):
98
 
        """Start writing the bundle"""
99
 
        self._fileobj.write(bundle_serializer._get_bundle_header(
100
 
            bundle_serializer.v4_string))
101
 
        self._fileobj.write('#\n')
102
 
        self._container.begin()
103
 
 
104
 
    def end(self):
105
 
        """Finish writing the bundle"""
106
 
        self._container.end()
107
 
        self._fileobj.write(self._compressor.flush())
108
 
 
109
 
    def add_multiparent_record(self, mp_bytes, sha1, parents, repo_kind,
110
 
                               revision_id, file_id):
111
 
        """Add a record for a multi-parent diff
112
 
 
113
 
        :mp_bytes: A multi-parent diff, as a bytestring
114
 
        :sha1: The sha1 hash of the fulltext
115
 
        :parents: a list of revision-ids of the parents
116
 
        :repo_kind: The kind of object in the repository.  May be 'file' or
117
 
            'inventory'
118
 
        :revision_id: The revision id of the mpdiff being added.
119
 
        :file_id: The file-id of the file, or None for inventories.
120
 
        """
121
 
        metadata = {'parents': parents,
122
 
                    'storage_kind': 'mpdiff',
123
 
                    'sha1': sha1}
124
 
        self._add_record(mp_bytes, metadata, repo_kind, revision_id, file_id)
125
 
 
126
 
    def add_fulltext_record(self, bytes, parents, repo_kind, revision_id):
127
 
        """Add a record for a fulltext
128
 
 
129
 
        :bytes: The fulltext, as a bytestring
130
 
        :parents: a list of revision-ids of the parents
131
 
        :repo_kind: The kind of object in the repository.  May be 'revision' or
132
 
            'signature'
133
 
        :revision_id: The revision id of the fulltext being added.
134
 
        """
135
 
        metadata = {'parents': parents,
136
 
                    'storage_kind': 'mpdiff'}
137
 
        self._add_record(bytes, {'parents': parents,
138
 
            'storage_kind': 'fulltext'}, repo_kind, revision_id, None)
139
 
 
140
 
    def add_info_record(self, **kwargs):
141
 
        """Add an info record to the bundle
142
 
 
143
 
        Any parameters may be supplied, except 'self' and 'storage_kind'.
144
 
        Values must be lists, strings, integers, dicts, or a combination.
145
 
        """
146
 
        kwargs['storage_kind'] = 'header'
147
 
        self._add_record(None, kwargs, 'info', None, None)
148
 
 
149
 
    @staticmethod
150
 
    def encode_name(content_kind, revision_id, file_id=None):
151
 
        """Encode semantic ids as a container name"""
152
 
        if content_kind not in ('revision', 'file', 'inventory', 'signature',
153
 
                'info'):
154
 
            raise ValueError(content_kind)
155
 
        if content_kind == 'file':
156
 
            if file_id is None:
157
 
                raise AssertionError()
158
 
        else:
159
 
            if file_id is not None:
160
 
                raise AssertionError()
161
 
        if content_kind == 'info':
162
 
            if revision_id is not None:
163
 
                raise AssertionError()
164
 
        elif revision_id is None:
165
 
            raise AssertionError()
166
 
        names = [n.replace('/', '//') for n in
167
 
                 (content_kind, revision_id, file_id) if n is not None]
168
 
        return '/'.join(names)
169
 
 
170
 
    def _add_record(self, bytes, metadata, repo_kind, revision_id, file_id):
171
 
        """Add a bundle record to the container.
172
 
 
173
 
        Most bundle records are recorded as header/body pairs, with the
174
 
        body being nameless.  Records with storage_kind 'header' have no
175
 
        body.
176
 
        """
177
 
        name = self.encode_name(repo_kind, revision_id, file_id)
178
 
        encoded_metadata = bencode.bencode(metadata)
179
 
        self._container.add_bytes_record(encoded_metadata, [(name, )])
180
 
        if metadata['storage_kind'] != 'header':
181
 
            self._container.add_bytes_record(bytes, [])
182
 
 
183
 
 
184
 
class BundleReader(object):
185
 
    """Reader for bundle-format files.
186
 
 
187
 
    This serves roughly the same purpose as ContainerReader, but acts as a
188
 
    layer on top of it, providing metadata, a semantic name, and a record
189
 
    body
190
 
    """
191
 
 
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
 
        """
200
 
        line = fileobj.readline()
201
 
        if line != '\n':
202
 
            fileobj.readline()
203
 
        self.patch_lines = []
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
209
 
 
210
 
    @staticmethod
211
 
    def iter_decode(fileobj):
212
 
        """Iterate through decoded fragments of the file"""
213
 
        decompressor = bz2.BZ2Decompressor()
214
 
        for line in fileobj:
215
 
            try:
216
 
                yield decompressor.decompress(line)
217
 
            except EOFError:
218
 
                return
219
 
 
220
 
    @staticmethod
221
 
    def decode_name(name):
222
 
        """Decode a name from its container form into a semantic form
223
 
 
224
 
        :retval: content_kind, revision_id, file_id
225
 
        """
226
 
        segments = re.split('(//?)', name)
227
 
        names = ['']
228
 
        for segment in segments:
229
 
            if segment == '//':
230
 
                names[-1] += '/'
231
 
            elif segment == '/':
232
 
                names.append('')
233
 
            else:
234
 
                names[-1] += segment
235
 
        content_kind = names[0]
236
 
        revision_id = None
237
 
        file_id = None
238
 
        if len(names) > 1:
239
 
            revision_id = names[1]
240
 
        if len(names) > 2:
241
 
            file_id = names[2]
242
 
        return content_kind, revision_id, file_id
243
 
 
244
 
    def iter_records(self):
245
 
        """Iterate through bundle records
246
 
 
247
 
        :return: a generator of (bytes, metadata, content_kind, revision_id,
248
 
            file_id)
249
 
        """
250
 
        iterator = pack.iter_records_from_file(self._container_file)
251
 
        for names, bytes in iterator:
252
 
            if len(names) != 1:
253
 
                raise errors.BadBundle('Record has %d names instead of 1'
254
 
                                       % len(names))
255
 
            metadata = bencode.bdecode(bytes)
256
 
            if metadata['storage_kind'] == 'header':
257
 
                bytes = None
258
 
            else:
259
 
                _unused, bytes = iterator.next()
260
 
            yield (bytes, metadata) + self.decode_name(names[0][0])
261
 
 
262
 
 
263
 
class BundleSerializerV4(bundle_serializer.BundleSerializer):
264
 
    """Implement the high-level bundle interface"""
265
 
 
266
 
    def write(self, repository, revision_ids, forced_bases, fileobj):
267
 
        """Write a bundle to a file-like object
268
 
 
269
 
        For backwards-compatibility only
270
 
        """
271
 
        write_op = BundleWriteOperation.from_old_args(repository, revision_ids,
272
 
                                                      forced_bases, fileobj)
273
 
        return write_op.do_write()
274
 
 
275
 
    def write_bundle(self, repository, target, base, fileobj):
276
 
        """Write a bundle to a file object
277
 
 
278
 
        :param repository: The repository to retrieve revision data from
279
 
        :param target: The head revision to include ancestors of
280
 
        :param base: The ancestor of the target to stop including acestors
281
 
            at.
282
 
        :param fileobj: The file-like object to write to
283
 
        """
284
 
        write_op =  BundleWriteOperation(base, target, repository, fileobj)
285
 
        return write_op.do_write()
286
 
 
287
 
    def read(self, file):
288
 
        """return a reader object for a given file"""
289
 
        bundle = BundleInfoV4(file, self)
290
 
        return bundle
291
 
 
292
 
    @staticmethod
293
 
    def get_source_serializer(info):
294
 
        """Retrieve the serializer for a given info object"""
295
 
        return serializer.format_registry.get(info['serializer'])
296
 
 
297
 
 
298
 
class BundleWriteOperation(object):
299
 
    """Perform the operation of writing revisions to a bundle"""
300
 
 
301
 
    @classmethod
302
 
    def from_old_args(cls, repository, revision_ids, forced_bases, fileobj):
303
 
        """Create a BundleWriteOperation from old-style arguments"""
304
 
        base, target = cls.get_base_target(revision_ids, forced_bases,
305
 
                                           repository)
306
 
        return BundleWriteOperation(base, target, repository, fileobj,
307
 
                                    revision_ids)
308
 
 
309
 
    def __init__(self, base, target, repository, fileobj, revision_ids=None):
310
 
        self.base = base
311
 
        self.target = target
312
 
        self.repository = repository
313
 
        bundle = BundleWriter(fileobj)
314
 
        self.bundle = bundle
315
 
        if revision_ids is not None:
316
 
            self.revision_ids = revision_ids
317
 
        else:
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])
324
 
 
325
 
    def do_write(self):
326
 
        """Write all data to the bundle"""
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()
338
 
        return self.revision_ids
339
 
 
340
 
    def write_info(self):
341
 
        """Write format info"""
342
 
        serializer_format = self.repository.get_serializer_format()
343
 
        supports_rich_root = {True: 1, False: 0}[
344
 
            self.repository.supports_rich_root()]
345
 
        self.bundle.add_info_record(serializer=serializer_format,
346
 
                                    supports_rich_root=supports_rich_root)
347
 
 
348
 
    def write_files(self):
349
 
        """Write bundle records for all revisions of all files"""
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)
357
 
 
358
 
    def write_revisions(self):
359
 
        """Write bundle records for all revisions and signatures"""
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
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)
367
 
            revision_order.remove(self.target)
368
 
            revision_order.append(self.target)
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)
407
 
            self.bundle.add_fulltext_record(revision_text, parents,
408
 
                                       'revision', revision_id)
409
 
            try:
410
 
                self.bundle.add_fulltext_record(
411
 
                    self.repository.get_signature_text(
412
 
                    revision_id), parents, 'signature', revision_id)
413
 
            except errors.NoSuchRevision:
414
 
                pass
415
 
 
416
 
    @staticmethod
417
 
    def get_base_target(revision_ids, forced_bases, repository):
418
 
        """Determine the base and target from old-style revision ids"""
419
 
        if len(revision_ids) == 0:
420
 
            return None, None
421
 
        target = revision_ids[0]
422
 
        base = forced_bases.get(target)
423
 
        if base is None:
424
 
            parents = repository.get_revision(target).parent_ids
425
 
            if len(parents) == 0:
426
 
                base = _mod_revision.NULL_REVISION
427
 
            else:
428
 
                base = parents[0]
429
 
        return base, target
430
 
 
431
 
    def _add_mp_records_keys(self, repo_kind, vf, keys):
432
 
        """Add multi-parent diff records to a bundle"""
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]]
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
446
 
            self.bundle.add_multiparent_record(text, sha1, parents, repo_kind,
447
 
                                               item_key[-1], file_id)
448
 
 
449
 
 
450
 
class BundleInfoV4(object):
451
 
 
452
 
    """Provide (most of) the BundleInfo interface"""
453
 
    def __init__(self, fileobj, serializer):
454
 
        self._fileobj = fileobj
455
 
        self._serializer = serializer
456
 
        self.__real_revisions = None
457
 
        self.__revisions = None
458
 
 
459
 
    def install(self, repository):
460
 
        return self.install_revisions(repository)
461
 
 
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
 
        """
470
 
        repository.lock_write()
471
 
        try:
472
 
            ri = RevisionInstaller(self.get_bundle_reader(stream_input),
473
 
                                   self._serializer, repository)
474
 
            return ri.install()
475
 
        finally:
476
 
            repository.unlock()
477
 
 
478
 
    def get_merge_request(self, target_repo):
479
 
        """Provide data for performing a merge
480
 
 
481
 
        Returns suggested base, suggested target, and patch verification status
482
 
        """
483
 
        return None, self.target, 'inapplicable'
484
 
 
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
 
        """
492
 
        self._fileobj.seek(0)
493
 
        return BundleReader(self._fileobj, stream_input)
494
 
 
495
 
    def _get_real_revisions(self):
496
 
        if self.__real_revisions is None:
497
 
            self.__real_revisions = []
498
 
            bundle_reader = self.get_bundle_reader()
499
 
            for bytes, metadata, repo_kind, revision_id, file_id in \
500
 
                bundle_reader.iter_records():
501
 
                if repo_kind == 'info':
502
 
                    serializer =\
503
 
                        self._serializer.get_source_serializer(metadata)
504
 
                if repo_kind == 'revision':
505
 
                    rev = serializer.read_revision_from_string(bytes)
506
 
                    self.__real_revisions.append(rev)
507
 
        return self.__real_revisions
508
 
    real_revisions = property(_get_real_revisions)
509
 
 
510
 
    def _get_revisions(self):
511
 
        if self.__revisions is None:
512
 
            self.__revisions = []
513
 
            for revision in self.real_revisions:
514
 
                self.__revisions.append(
515
 
                    bundle_data.RevisionInfo.from_revision(revision))
516
 
        return self.__revisions
517
 
 
518
 
    revisions = property(_get_revisions)
519
 
 
520
 
    def _get_target(self):
521
 
        return self.revisions[-1].revision_id
522
 
 
523
 
    target = property(_get_target)
524
 
 
525
 
 
526
 
class RevisionInstaller(object):
527
 
    """Installs revisions into a repository"""
528
 
 
529
 
    def __init__(self, container, serializer, repository):
530
 
        self._container = container
531
 
        self._serializer = serializer
532
 
        self._repository = repository
533
 
        self._info = None
534
 
 
535
 
    def install(self):
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):
550
 
        current_file = None
551
 
        current_versionedfile = None
552
 
        pending_file_records = []
553
 
        inventory_vf = None
554
 
        pending_inventory_records = []
555
 
        added_inv = set()
556
 
        target_revision = None
557
 
        for bytes, metadata, repo_kind, revision_id, file_id in\
558
 
            self._container.iter_records():
559
 
            if repo_kind == 'info':
560
 
                if self._info is not None:
561
 
                    raise AssertionError()
562
 
                self._handle_info(metadata)
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,
568
 
                    pending_file_records)
569
 
                current_file = None
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)
581
 
            if repo_kind == 'file':
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)
585
 
        return target_revision
586
 
 
587
 
    def _handle_info(self, info):
588
 
        """Extract data from an info record"""
589
 
        self._info = info
590
 
        self._source_serializer = self._serializer.get_source_serializer(info)
591
 
        if (info['supports_rich_root'] == 0 and
592
 
            self._repository.supports_rich_root()):
593
 
            self.update_root = True
594
 
        else:
595
 
            self.update_root = False
596
 
 
597
 
    def _install_mp_records(self, versionedfile, records):
598
 
        if len(records) == 0:
599
 
            return
600
 
        d_func = multiparent.MultiParent.from_patch
601
 
        vf_records = [(r, m['parents'], m['sha1'], d_func(t)) for r, m, t in
602
 
                      records if r not in versionedfile]
603
 
        versionedfile.add_mpdiffs(vf_records)
604
 
 
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()
677
 
        try:
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()
720
 
 
721
 
    def _handle_root(self, target_inv, parent_ids):
722
 
        revision_id = target_inv.revision_id
723
 
        if self.update_root:
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, [])
728
 
        elif not self._repository.supports_rich_root():
729
 
            if target_inv.root.revision != revision_id:
730
 
                raise errors.IncompatibleRevision(repr(self._repository))
731
 
 
732
 
    def _install_revision(self, revision_id, metadata, text):
733
 
        if self._repository.has_revision(revision_id):
734
 
            return
735
 
        revision = self._source_serializer.read_revision_from_string(text)
736
 
        self._repository.add_revision(revision.revision_id, revision)
737
 
 
738
 
    def _install_signature(self, revision_id, metadata, text):
739
 
        transaction = self._repository.get_transaction()
740
 
        if self._repository.has_signature_for_revision_id(revision_id):
741
 
            return
742
 
        self._repository.add_signature_text(revision_id, text)