/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 bzrlib/bundle/serializer/v4.py

  • Committer: John Arbash Meinel
  • Date: 2009-12-22 16:28:47 UTC
  • mto: This revision was merged to the branch mainline in revision 4922.
  • Revision ID: john@arbash-meinel.com-20091222162847-tvnsc69to4l4uf5r
Implement a permute_for_extension helper.

Use it for all of the 'simple' extension permutations.
It basically permutes all tests in the current module, by setting TestCase.module.
Which works well for most of our extension tests. Some had more advanced
handling of permutations (extra permutations, custom vars, etc.)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2007 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 cStringIO import StringIO
 
18
import bz2
 
19
import re
 
20
 
 
21
from bzrlib import (
 
22
    diff,
 
23
    errors,
 
24
    iterablefile,
 
25
    lru_cache,
 
26
    multiparent,
 
27
    osutils,
 
28
    pack,
 
29
    revision as _mod_revision,
 
30
    serializer,
 
31
    trace,
 
32
    ui,
 
33
    )
 
34
from bzrlib.bundle import bundle_data, serializer as bundle_serializer
 
35
from bzrlib import bencode
 
36
 
 
37
 
 
38
class BundleWriter(object):
 
39
    """Writer for bundle-format files.
 
40
 
 
41
    This serves roughly the same purpose as ContainerReader, but acts as a
 
42
    layer on top of it.
 
43
 
 
44
    Provides ways of writing the specific record types supported this bundle
 
45
    format.
 
46
    """
 
47
 
 
48
    def __init__(self, fileobj):
 
49
        self._container = pack.ContainerWriter(self._write_encoded)
 
50
        self._fileobj = fileobj
 
51
        self._compressor = bz2.BZ2Compressor()
 
52
 
 
53
    def _write_encoded(self, bytes):
 
54
        """Write bzip2-encoded bytes to the file"""
 
55
        self._fileobj.write(self._compressor.compress(bytes))
 
56
 
 
57
    def begin(self):
 
58
        """Start writing the bundle"""
 
59
        self._fileobj.write(bundle_serializer._get_bundle_header(
 
60
            bundle_serializer.v4_string))
 
61
        self._fileobj.write('#\n')
 
62
        self._container.begin()
 
63
 
 
64
    def end(self):
 
65
        """Finish writing the bundle"""
 
66
        self._container.end()
 
67
        self._fileobj.write(self._compressor.flush())
 
68
 
 
69
    def add_multiparent_record(self, mp_bytes, sha1, parents, repo_kind,
 
70
                               revision_id, file_id):
 
71
        """Add a record for a multi-parent diff
 
72
 
 
73
        :mp_bytes: A multi-parent diff, as a bytestring
 
74
        :sha1: The sha1 hash of the fulltext
 
75
        :parents: a list of revision-ids of the parents
 
76
        :repo_kind: The kind of object in the repository.  May be 'file' or
 
77
            'inventory'
 
78
        :revision_id: The revision id of the mpdiff being added.
 
79
        :file_id: The file-id of the file, or None for inventories.
 
80
        """
 
81
        metadata = {'parents': parents,
 
82
                    'storage_kind': 'mpdiff',
 
83
                    'sha1': sha1}
 
84
        self._add_record(mp_bytes, metadata, repo_kind, revision_id, file_id)
 
85
 
 
86
    def add_fulltext_record(self, bytes, parents, repo_kind, revision_id):
 
87
        """Add a record for a fulltext
 
88
 
 
89
        :bytes: The fulltext, as a bytestring
 
90
        :parents: a list of revision-ids of the parents
 
91
        :repo_kind: The kind of object in the repository.  May be 'revision' or
 
92
            'signature'
 
93
        :revision_id: The revision id of the fulltext being added.
 
94
        """
 
95
        metadata = {'parents': parents,
 
96
                    'storage_kind': 'mpdiff'}
 
97
        self._add_record(bytes, {'parents': parents,
 
98
            'storage_kind': 'fulltext'}, repo_kind, revision_id, None)
 
99
 
 
100
    def add_info_record(self, **kwargs):
 
101
        """Add an info record to the bundle
 
102
 
 
103
        Any parameters may be supplied, except 'self' and 'storage_kind'.
 
104
        Values must be lists, strings, integers, dicts, or a combination.
 
105
        """
 
106
        kwargs['storage_kind'] = 'header'
 
107
        self._add_record(None, kwargs, 'info', None, None)
 
108
 
 
109
    @staticmethod
 
110
    def encode_name(content_kind, revision_id, file_id=None):
 
111
        """Encode semantic ids as a container name"""
 
112
        if content_kind not in ('revision', 'file', 'inventory', 'signature',
 
113
                'info'):
 
114
            raise ValueError(content_kind)
 
115
        if content_kind == 'file':
 
116
            if file_id is None:
 
117
                raise AssertionError()
 
118
        else:
 
119
            if file_id is not None:
 
120
                raise AssertionError()
 
121
        if content_kind == 'info':
 
122
            if revision_id is not None:
 
123
                raise AssertionError()
 
124
        elif revision_id is None:
 
125
            raise AssertionError()
 
126
        names = [n.replace('/', '//') for n in
 
127
                 (content_kind, revision_id, file_id) if n is not None]
 
128
        return '/'.join(names)
 
129
 
 
130
    def _add_record(self, bytes, metadata, repo_kind, revision_id, file_id):
 
131
        """Add a bundle record to the container.
 
132
 
 
133
        Most bundle records are recorded as header/body pairs, with the
 
134
        body being nameless.  Records with storage_kind 'header' have no
 
135
        body.
 
136
        """
 
137
        name = self.encode_name(repo_kind, revision_id, file_id)
 
138
        encoded_metadata = bencode.bencode(metadata)
 
139
        self._container.add_bytes_record(encoded_metadata, [(name, )])
 
140
        if metadata['storage_kind'] != 'header':
 
141
            self._container.add_bytes_record(bytes, [])
 
142
 
 
143
 
 
144
class BundleReader(object):
 
145
    """Reader for bundle-format files.
 
146
 
 
147
    This serves roughly the same purpose as ContainerReader, but acts as a
 
148
    layer on top of it, providing metadata, a semantic name, and a record
 
149
    body
 
150
    """
 
151
 
 
152
    def __init__(self, fileobj, stream_input=True):
 
153
        """Constructor
 
154
 
 
155
        :param fileobj: a file containing a bzip-encoded container
 
156
        :param stream_input: If True, the BundleReader stream input rather than
 
157
            reading it all into memory at once.  Reading it into memory all at
 
158
            once is (currently) faster.
 
159
        """
 
160
        line = fileobj.readline()
 
161
        if line != '\n':
 
162
            fileobj.readline()
 
163
        self.patch_lines = []
 
164
        if stream_input:
 
165
            source_file = iterablefile.IterableFile(self.iter_decode(fileobj))
 
166
        else:
 
167
            source_file = StringIO(bz2.decompress(fileobj.read()))
 
168
        self._container_file = source_file
 
169
 
 
170
    @staticmethod
 
171
    def iter_decode(fileobj):
 
172
        """Iterate through decoded fragments of the file"""
 
173
        decompressor = bz2.BZ2Decompressor()
 
174
        for line in fileobj:
 
175
            try:
 
176
                yield decompressor.decompress(line)
 
177
            except EOFError:
 
178
                return
 
179
 
 
180
    @staticmethod
 
181
    def decode_name(name):
 
182
        """Decode a name from its container form into a semantic form
 
183
 
 
184
        :retval: content_kind, revision_id, file_id
 
185
        """
 
186
        segments = re.split('(//?)', name)
 
187
        names = ['']
 
188
        for segment in segments:
 
189
            if segment == '//':
 
190
                names[-1] += '/'
 
191
            elif segment == '/':
 
192
                names.append('')
 
193
            else:
 
194
                names[-1] += segment
 
195
        content_kind = names[0]
 
196
        revision_id = None
 
197
        file_id = None
 
198
        if len(names) > 1:
 
199
            revision_id = names[1]
 
200
        if len(names) > 2:
 
201
            file_id = names[2]
 
202
        return content_kind, revision_id, file_id
 
203
 
 
204
    def iter_records(self):
 
205
        """Iterate through bundle records
 
206
 
 
207
        :return: a generator of (bytes, metadata, content_kind, revision_id,
 
208
            file_id)
 
209
        """
 
210
        iterator = pack.iter_records_from_file(self._container_file)
 
211
        for names, bytes in iterator:
 
212
            if len(names) != 1:
 
213
                raise errors.BadBundle('Record has %d names instead of 1'
 
214
                                       % len(names))
 
215
            metadata = bencode.bdecode(bytes)
 
216
            if metadata['storage_kind'] == 'header':
 
217
                bytes = None
 
218
            else:
 
219
                _unused, bytes = iterator.next()
 
220
            yield (bytes, metadata) + self.decode_name(names[0][0])
 
221
 
 
222
 
 
223
class BundleSerializerV4(bundle_serializer.BundleSerializer):
 
224
    """Implement the high-level bundle interface"""
 
225
 
 
226
    def write(self, repository, revision_ids, forced_bases, fileobj):
 
227
        """Write a bundle to a file-like object
 
228
 
 
229
        For backwards-compatibility only
 
230
        """
 
231
        write_op = BundleWriteOperation.from_old_args(repository, revision_ids,
 
232
                                                      forced_bases, fileobj)
 
233
        return write_op.do_write()
 
234
 
 
235
    def write_bundle(self, repository, target, base, fileobj):
 
236
        """Write a bundle to a file object
 
237
 
 
238
        :param repository: The repository to retrieve revision data from
 
239
        :param target: The head revision to include ancestors of
 
240
        :param base: The ancestor of the target to stop including acestors
 
241
            at.
 
242
        :param fileobj: The file-like object to write to
 
243
        """
 
244
        write_op =  BundleWriteOperation(base, target, repository, fileobj)
 
245
        return write_op.do_write()
 
246
 
 
247
    def read(self, file):
 
248
        """return a reader object for a given file"""
 
249
        bundle = BundleInfoV4(file, self)
 
250
        return bundle
 
251
 
 
252
    @staticmethod
 
253
    def get_source_serializer(info):
 
254
        """Retrieve the serializer for a given info object"""
 
255
        return serializer.format_registry.get(info['serializer'])
 
256
 
 
257
 
 
258
class BundleWriteOperation(object):
 
259
    """Perform the operation of writing revisions to a bundle"""
 
260
 
 
261
    @classmethod
 
262
    def from_old_args(cls, repository, revision_ids, forced_bases, fileobj):
 
263
        """Create a BundleWriteOperation from old-style arguments"""
 
264
        base, target = cls.get_base_target(revision_ids, forced_bases,
 
265
                                           repository)
 
266
        return BundleWriteOperation(base, target, repository, fileobj,
 
267
                                    revision_ids)
 
268
 
 
269
    def __init__(self, base, target, repository, fileobj, revision_ids=None):
 
270
        self.base = base
 
271
        self.target = target
 
272
        self.repository = repository
 
273
        bundle = BundleWriter(fileobj)
 
274
        self.bundle = bundle
 
275
        if revision_ids is not None:
 
276
            self.revision_ids = revision_ids
 
277
        else:
 
278
            graph = repository.get_graph()
 
279
            revision_ids = graph.find_unique_ancestors(target, [base])
 
280
            # Strip ghosts
 
281
            parents = graph.get_parent_map(revision_ids)
 
282
            self.revision_ids = [r for r in revision_ids if r in parents]
 
283
        self.revision_keys = set([(revid,) for revid in self.revision_ids])
 
284
 
 
285
    def do_write(self):
 
286
        """Write all data to the bundle"""
 
287
        trace.note('Bundling %d revision(s).', len(self.revision_ids))
 
288
        self.repository.lock_read()
 
289
        try:
 
290
            self.bundle.begin()
 
291
            self.write_info()
 
292
            self.write_files()
 
293
            self.write_revisions()
 
294
            self.bundle.end()
 
295
        finally:
 
296
            self.repository.unlock()
 
297
        return self.revision_ids
 
298
 
 
299
    def write_info(self):
 
300
        """Write format info"""
 
301
        serializer_format = self.repository.get_serializer_format()
 
302
        supports_rich_root = {True: 1, False: 0}[
 
303
            self.repository.supports_rich_root()]
 
304
        self.bundle.add_info_record(serializer=serializer_format,
 
305
                                    supports_rich_root=supports_rich_root)
 
306
 
 
307
    def write_files(self):
 
308
        """Write bundle records for all revisions of all files"""
 
309
        text_keys = []
 
310
        altered_fileids = self.repository.fileids_altered_by_revision_ids(
 
311
                self.revision_ids)
 
312
        for file_id, revision_ids in altered_fileids.iteritems():
 
313
            for revision_id in revision_ids:
 
314
                text_keys.append((file_id, revision_id))
 
315
        self._add_mp_records_keys('file', self.repository.texts, text_keys)
 
316
 
 
317
    def write_revisions(self):
 
318
        """Write bundle records for all revisions and signatures"""
 
319
        inv_vf = self.repository.inventories
 
320
        topological_order = [key[-1] for key in multiparent.topo_iter_keys(
 
321
                                inv_vf, self.revision_keys)]
 
322
        revision_order = topological_order
 
323
        if self.target is not None and self.target in self.revision_ids:
 
324
            # Make sure the target revision is always the last entry
 
325
            revision_order = list(topological_order)
 
326
            revision_order.remove(self.target)
 
327
            revision_order.append(self.target)
 
328
        if self.repository._serializer.support_altered_by_hack:
 
329
            # Repositories that support_altered_by_hack means that
 
330
            # inventories.make_mpdiffs() contains all the data about the tree
 
331
            # shape. Formats without support_altered_by_hack require
 
332
            # chk_bytes/etc, so we use a different code path.
 
333
            self._add_mp_records_keys('inventory', inv_vf,
 
334
                                      [(revid,) for revid in topological_order])
 
335
        else:
 
336
            # Inventories should always be added in pure-topological order, so
 
337
            # that we can apply the mpdiff for the child to the parent texts.
 
338
            self._add_inventory_mpdiffs_from_serializer(topological_order)
 
339
        self._add_revision_texts(revision_order)
 
340
 
 
341
    def _add_inventory_mpdiffs_from_serializer(self, revision_order):
 
342
        """Generate mpdiffs by serializing inventories.
 
343
 
 
344
        The current repository only has part of the tree shape information in
 
345
        the 'inventories' vf. So we use serializer.write_inventory_to_string to
 
346
        get a 'full' representation of the tree shape, and then generate
 
347
        mpdiffs on that data stream. This stream can then be reconstructed on
 
348
        the other side.
 
349
        """
 
350
        inventory_key_order = [(r,) for r in revision_order]
 
351
        parent_map = self.repository.inventories.get_parent_map(
 
352
                            inventory_key_order)
 
353
        missing_keys = set(inventory_key_order).difference(parent_map)
 
354
        if missing_keys:
 
355
            raise errors.RevisionNotPresent(list(missing_keys)[0],
 
356
                                            self.repository.inventories)
 
357
        inv_to_str = self.repository._serializer.write_inventory_to_string
 
358
        # Make sure that we grab the parent texts first
 
359
        just_parents = set()
 
360
        map(just_parents.update, parent_map.itervalues())
 
361
        just_parents.difference_update(parent_map)
 
362
        # Ignore ghost parents
 
363
        present_parents = self.repository.inventories.get_parent_map(
 
364
                            just_parents)
 
365
        ghost_keys = just_parents.difference(present_parents)
 
366
        needed_inventories = list(present_parents) + inventory_key_order
 
367
        needed_inventories = [k[-1] for k in needed_inventories]
 
368
        all_lines = {}
 
369
        for inv in self.repository.iter_inventories(needed_inventories):
 
370
            revision_id = inv.revision_id
 
371
            key = (revision_id,)
 
372
            as_bytes = inv_to_str(inv)
 
373
            # The sha1 is validated as the xml/textual form, not as the
 
374
            # form-in-the-repository
 
375
            sha1 = osutils.sha_string(as_bytes)
 
376
            as_lines = osutils.split_lines(as_bytes)
 
377
            del as_bytes
 
378
            all_lines[key] = as_lines
 
379
            if key in just_parents:
 
380
                # We don't transmit those entries
 
381
                continue
 
382
            # Create an mpdiff for this text, and add it to the output
 
383
            parent_keys = parent_map[key]
 
384
            # See the comment in VF.make_mpdiffs about how this effects
 
385
            # ordering when there are ghosts present. I think we have a latent
 
386
            # bug
 
387
            parent_lines = [all_lines[p_key] for p_key in parent_keys
 
388
                            if p_key not in ghost_keys]
 
389
            diff = multiparent.MultiParent.from_lines(
 
390
                as_lines, parent_lines)
 
391
            text = ''.join(diff.to_patch())
 
392
            parent_ids = [k[-1] for k in parent_keys]
 
393
            self.bundle.add_multiparent_record(text, sha1, parent_ids,
 
394
                                               'inventory', revision_id, None)
 
395
 
 
396
    def _add_revision_texts(self, revision_order):
 
397
        parent_map = self.repository.get_parent_map(revision_order)
 
398
        revision_to_str = self.repository._serializer.write_revision_to_string
 
399
        revisions = self.repository.get_revisions(revision_order)
 
400
        for revision in revisions:
 
401
            revision_id = revision.revision_id
 
402
            parents = parent_map.get(revision_id, None)
 
403
            revision_text = revision_to_str(revision)
 
404
            self.bundle.add_fulltext_record(revision_text, parents,
 
405
                                       'revision', revision_id)
 
406
            try:
 
407
                self.bundle.add_fulltext_record(
 
408
                    self.repository.get_signature_text(
 
409
                    revision_id), parents, 'signature', revision_id)
 
410
            except errors.NoSuchRevision:
 
411
                pass
 
412
 
 
413
    @staticmethod
 
414
    def get_base_target(revision_ids, forced_bases, repository):
 
415
        """Determine the base and target from old-style revision ids"""
 
416
        if len(revision_ids) == 0:
 
417
            return None, None
 
418
        target = revision_ids[0]
 
419
        base = forced_bases.get(target)
 
420
        if base is None:
 
421
            parents = repository.get_revision(target).parent_ids
 
422
            if len(parents) == 0:
 
423
                base = _mod_revision.NULL_REVISION
 
424
            else:
 
425
                base = parents[0]
 
426
        return base, target
 
427
 
 
428
    def _add_mp_records_keys(self, repo_kind, vf, keys):
 
429
        """Add multi-parent diff records to a bundle"""
 
430
        ordered_keys = list(multiparent.topo_iter_keys(vf, keys))
 
431
        mpdiffs = vf.make_mpdiffs(ordered_keys)
 
432
        sha1s = vf.get_sha1s(ordered_keys)
 
433
        parent_map = vf.get_parent_map(ordered_keys)
 
434
        for mpdiff, item_key, in zip(mpdiffs, ordered_keys):
 
435
            sha1 = sha1s[item_key]
 
436
            parents = [key[-1] for key in parent_map[item_key]]
 
437
            text = ''.join(mpdiff.to_patch())
 
438
            # Infer file id records as appropriate.
 
439
            if len(item_key) == 2:
 
440
                file_id = item_key[0]
 
441
            else:
 
442
                file_id = None
 
443
            self.bundle.add_multiparent_record(text, sha1, parents, repo_kind,
 
444
                                               item_key[-1], file_id)
 
445
 
 
446
 
 
447
class BundleInfoV4(object):
 
448
 
 
449
    """Provide (most of) the BundleInfo interface"""
 
450
    def __init__(self, fileobj, serializer):
 
451
        self._fileobj = fileobj
 
452
        self._serializer = serializer
 
453
        self.__real_revisions = None
 
454
        self.__revisions = None
 
455
 
 
456
    def install(self, repository):
 
457
        return self.install_revisions(repository)
 
458
 
 
459
    def install_revisions(self, repository, stream_input=True):
 
460
        """Install this bundle's revisions into the specified repository
 
461
 
 
462
        :param target_repo: The repository to install into
 
463
        :param stream_input: If True, will stream input rather than reading it
 
464
            all into memory at once.  Reading it into memory all at once is
 
465
            (currently) faster.
 
466
        """
 
467
        repository.lock_write()
 
468
        try:
 
469
            ri = RevisionInstaller(self.get_bundle_reader(stream_input),
 
470
                                   self._serializer, repository)
 
471
            return ri.install()
 
472
        finally:
 
473
            repository.unlock()
 
474
 
 
475
    def get_merge_request(self, target_repo):
 
476
        """Provide data for performing a merge
 
477
 
 
478
        Returns suggested base, suggested target, and patch verification status
 
479
        """
 
480
        return None, self.target, 'inapplicable'
 
481
 
 
482
    def get_bundle_reader(self, stream_input=True):
 
483
        """Return a new BundleReader for the associated bundle
 
484
 
 
485
        :param stream_input: If True, the BundleReader stream input rather than
 
486
            reading it all into memory at once.  Reading it into memory all at
 
487
            once is (currently) faster.
 
488
        """
 
489
        self._fileobj.seek(0)
 
490
        return BundleReader(self._fileobj, stream_input)
 
491
 
 
492
    def _get_real_revisions(self):
 
493
        if self.__real_revisions is None:
 
494
            self.__real_revisions = []
 
495
            bundle_reader = self.get_bundle_reader()
 
496
            for bytes, metadata, repo_kind, revision_id, file_id in \
 
497
                bundle_reader.iter_records():
 
498
                if repo_kind == 'info':
 
499
                    serializer =\
 
500
                        self._serializer.get_source_serializer(metadata)
 
501
                if repo_kind == 'revision':
 
502
                    rev = serializer.read_revision_from_string(bytes)
 
503
                    self.__real_revisions.append(rev)
 
504
        return self.__real_revisions
 
505
    real_revisions = property(_get_real_revisions)
 
506
 
 
507
    def _get_revisions(self):
 
508
        if self.__revisions is None:
 
509
            self.__revisions = []
 
510
            for revision in self.real_revisions:
 
511
                self.__revisions.append(
 
512
                    bundle_data.RevisionInfo.from_revision(revision))
 
513
        return self.__revisions
 
514
 
 
515
    revisions = property(_get_revisions)
 
516
 
 
517
    def _get_target(self):
 
518
        return self.revisions[-1].revision_id
 
519
 
 
520
    target = property(_get_target)
 
521
 
 
522
 
 
523
class RevisionInstaller(object):
 
524
    """Installs revisions into a repository"""
 
525
 
 
526
    def __init__(self, container, serializer, repository):
 
527
        self._container = container
 
528
        self._serializer = serializer
 
529
        self._repository = repository
 
530
        self._info = None
 
531
 
 
532
    def install(self):
 
533
        """Perform the installation.
 
534
 
 
535
        Must be called with the Repository locked.
 
536
        """
 
537
        self._repository.start_write_group()
 
538
        try:
 
539
            result = self._install_in_write_group()
 
540
        except:
 
541
            self._repository.abort_write_group()
 
542
            raise
 
543
        self._repository.commit_write_group()
 
544
        return result
 
545
 
 
546
    def _install_in_write_group(self):
 
547
        current_file = None
 
548
        current_versionedfile = None
 
549
        pending_file_records = []
 
550
        inventory_vf = None
 
551
        pending_inventory_records = []
 
552
        added_inv = set()
 
553
        target_revision = None
 
554
        for bytes, metadata, repo_kind, revision_id, file_id in\
 
555
            self._container.iter_records():
 
556
            if repo_kind == 'info':
 
557
                if self._info is not None:
 
558
                    raise AssertionError()
 
559
                self._handle_info(metadata)
 
560
            if (pending_file_records and
 
561
                (repo_kind, file_id) != ('file', current_file)):
 
562
                # Flush the data for a single file - prevents memory
 
563
                # spiking due to buffering all files in memory.
 
564
                self._install_mp_records_keys(self._repository.texts,
 
565
                    pending_file_records)
 
566
                current_file = None
 
567
                del pending_file_records[:]
 
568
            if len(pending_inventory_records) > 0 and repo_kind != 'inventory':
 
569
                self._install_inventory_records(pending_inventory_records)
 
570
                pending_inventory_records = []
 
571
            if repo_kind == 'inventory':
 
572
                pending_inventory_records.append(((revision_id,), metadata, bytes))
 
573
            if repo_kind == 'revision':
 
574
                target_revision = revision_id
 
575
                self._install_revision(revision_id, metadata, bytes)
 
576
            if repo_kind == 'signature':
 
577
                self._install_signature(revision_id, metadata, bytes)
 
578
            if repo_kind == 'file':
 
579
                current_file = file_id
 
580
                pending_file_records.append(((file_id, revision_id), metadata, bytes))
 
581
        self._install_mp_records_keys(self._repository.texts, pending_file_records)
 
582
        return target_revision
 
583
 
 
584
    def _handle_info(self, info):
 
585
        """Extract data from an info record"""
 
586
        self._info = info
 
587
        self._source_serializer = self._serializer.get_source_serializer(info)
 
588
        if (info['supports_rich_root'] == 0 and
 
589
            self._repository.supports_rich_root()):
 
590
            self.update_root = True
 
591
        else:
 
592
            self.update_root = False
 
593
 
 
594
    def _install_mp_records(self, versionedfile, records):
 
595
        if len(records) == 0:
 
596
            return
 
597
        d_func = multiparent.MultiParent.from_patch
 
598
        vf_records = [(r, m['parents'], m['sha1'], d_func(t)) for r, m, t in
 
599
                      records if r not in versionedfile]
 
600
        versionedfile.add_mpdiffs(vf_records)
 
601
 
 
602
    def _install_mp_records_keys(self, versionedfile, records):
 
603
        d_func = multiparent.MultiParent.from_patch
 
604
        vf_records = []
 
605
        for key, meta, text in records:
 
606
            # Adapt to tuple interface: A length two key is a file_id,
 
607
            # revision_id pair, a length 1 key is a
 
608
            # revision/signature/inventory. We need to do this because
 
609
            # the metadata extraction from the bundle has not yet been updated
 
610
            # to use the consistent tuple interface itself.
 
611
            if len(key) == 2:
 
612
                prefix = key[:1]
 
613
            else:
 
614
                prefix = ()
 
615
            parents = [prefix + (parent,) for parent in meta['parents']]
 
616
            vf_records.append((key, parents, meta['sha1'], d_func(text)))
 
617
        versionedfile.add_mpdiffs(vf_records)
 
618
 
 
619
    def _get_parent_inventory_texts(self, inventory_text_cache,
 
620
                                    inventory_cache, parent_ids):
 
621
        cached_parent_texts = {}
 
622
        remaining_parent_ids = []
 
623
        for parent_id in parent_ids:
 
624
            p_text = inventory_text_cache.get(parent_id, None)
 
625
            if p_text is None:
 
626
                remaining_parent_ids.append(parent_id)
 
627
            else:
 
628
                cached_parent_texts[parent_id] = p_text
 
629
        ghosts = ()
 
630
        # TODO: Use inventory_cache to grab inventories we already have in
 
631
        #       memory
 
632
        if remaining_parent_ids:
 
633
            # first determine what keys are actually present in the local
 
634
            # inventories object (don't use revisions as they haven't been
 
635
            # installed yet.)
 
636
            parent_keys = [(r,) for r in remaining_parent_ids]
 
637
            present_parent_map = self._repository.inventories.get_parent_map(
 
638
                                        parent_keys)
 
639
            present_parent_ids = []
 
640
            ghosts = set()
 
641
            for p_id in remaining_parent_ids:
 
642
                if (p_id,) in present_parent_map:
 
643
                    present_parent_ids.append(p_id)
 
644
                else:
 
645
                    ghosts.add(p_id)
 
646
            to_string = self._source_serializer.write_inventory_to_string
 
647
            for parent_inv in self._repository.iter_inventories(
 
648
                                    present_parent_ids):
 
649
                p_text = to_string(parent_inv)
 
650
                inventory_cache[parent_inv.revision_id] = parent_inv
 
651
                cached_parent_texts[parent_inv.revision_id] = p_text
 
652
                inventory_text_cache[parent_inv.revision_id] = p_text
 
653
 
 
654
        parent_texts = [cached_parent_texts[parent_id]
 
655
                        for parent_id in parent_ids
 
656
                         if parent_id not in ghosts]
 
657
        return parent_texts
 
658
 
 
659
    def _install_inventory_records(self, records):
 
660
        if (self._info['serializer'] == self._repository._serializer.format_num
 
661
            and self._repository._serializer.support_altered_by_hack):
 
662
            return self._install_mp_records_keys(self._repository.inventories,
 
663
                records)
 
664
        # Use a 10MB text cache, since these are string xml inventories. Note
 
665
        # that 10MB is fairly small for large projects (a single inventory can
 
666
        # be >5MB). Another possibility is to cache 10-20 inventory texts
 
667
        # instead
 
668
        inventory_text_cache = lru_cache.LRUSizeCache(10*1024*1024)
 
669
        # Also cache the in-memory representation. This allows us to create
 
670
        # inventory deltas to apply rather than calling add_inventory from
 
671
        # scratch each time.
 
672
        inventory_cache = lru_cache.LRUCache(10)
 
673
        pb = ui.ui_factory.nested_progress_bar()
 
674
        try:
 
675
            num_records = len(records)
 
676
            for idx, (key, metadata, bytes) in enumerate(records):
 
677
                pb.update('installing inventory', idx, num_records)
 
678
                revision_id = key[-1]
 
679
                parent_ids = metadata['parents']
 
680
                # Note: This assumes the local ghosts are identical to the
 
681
                #       ghosts in the source, as the Bundle serialization
 
682
                #       format doesn't record ghosts.
 
683
                p_texts = self._get_parent_inventory_texts(inventory_text_cache,
 
684
                                                           inventory_cache,
 
685
                                                           parent_ids)
 
686
                # Why does to_lines() take strings as the source, it seems that
 
687
                # it would have to cast to a list of lines, which we get back
 
688
                # as lines and then cast back to a string.
 
689
                target_lines = multiparent.MultiParent.from_patch(bytes
 
690
                            ).to_lines(p_texts)
 
691
                inv_text = ''.join(target_lines)
 
692
                del target_lines
 
693
                sha1 = osutils.sha_string(inv_text)
 
694
                if sha1 != metadata['sha1']:
 
695
                    raise errors.BadBundle("Can't convert to target format")
 
696
                # Add this to the cache so we don't have to extract it again.
 
697
                inventory_text_cache[revision_id] = inv_text
 
698
                target_inv = self._source_serializer.read_inventory_from_string(
 
699
                    inv_text)
 
700
                self._handle_root(target_inv, parent_ids)
 
701
                parent_inv = None
 
702
                if parent_ids:
 
703
                    parent_inv = inventory_cache.get(parent_ids[0], None)
 
704
                try:
 
705
                    if parent_inv is None:
 
706
                        self._repository.add_inventory(revision_id, target_inv,
 
707
                                                       parent_ids)
 
708
                    else:
 
709
                        delta = target_inv._make_delta(parent_inv)
 
710
                        self._repository.add_inventory_by_delta(parent_ids[0],
 
711
                            delta, revision_id, parent_ids)
 
712
                except errors.UnsupportedInventoryKind:
 
713
                    raise errors.IncompatibleRevision(repr(self._repository))
 
714
                inventory_cache[revision_id] = target_inv
 
715
        finally:
 
716
            pb.finished()
 
717
 
 
718
    def _handle_root(self, target_inv, parent_ids):
 
719
        revision_id = target_inv.revision_id
 
720
        if self.update_root:
 
721
            text_key = (target_inv.root.file_id, revision_id)
 
722
            parent_keys = [(target_inv.root.file_id, parent) for
 
723
                parent in parent_ids]
 
724
            self._repository.texts.add_lines(text_key, parent_keys, [])
 
725
        elif not self._repository.supports_rich_root():
 
726
            if target_inv.root.revision != revision_id:
 
727
                raise errors.IncompatibleRevision(repr(self._repository))
 
728
 
 
729
    def _install_revision(self, revision_id, metadata, text):
 
730
        if self._repository.has_revision(revision_id):
 
731
            return
 
732
        revision = self._source_serializer.read_revision_from_string(text)
 
733
        self._repository.add_revision(revision.revision_id, revision)
 
734
 
 
735
    def _install_signature(self, revision_id, metadata, text):
 
736
        transaction = self._repository.get_transaction()
 
737
        if self._repository.has_signature_for_revision_id(revision_id):
 
738
            return
 
739
        self._repository.add_signature_text(revision_id, text)