/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: 2008-11-25 18:51:48 UTC
  • mto: This revision was merged to the branch mainline in revision 3854.
  • Revision ID: john@arbash-meinel.com-20081125185148-jsfkqnzfjjqsleds
It seems we have some direct tests that don't use strings and expect a value error as well.

They would be sanitized later on by Revision. We could use that code, but this test
depends on the serializer, which Revision wouldn't know about.

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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  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
    multiparent,
 
26
    osutils,
 
27
    pack,
 
28
    revision as _mod_revision,
 
29
    trace,
 
30
    xml_serializer,
 
31
    )
 
32
from bzrlib.bundle import bundle_data, serializer
 
33
from bzrlib.util import bencode
 
34
 
 
35
 
 
36
class BundleWriter(object):
 
37
    """Writer for bundle-format files.
 
38
 
 
39
    This serves roughly the same purpose as ContainerReader, but acts as a
 
40
    layer on top of it.
 
41
 
 
42
    Provides ways of writing the specific record types supported this bundle
 
43
    format.
 
44
    """
 
45
 
 
46
    def __init__(self, fileobj):
 
47
        self._container = pack.ContainerWriter(self._write_encoded)
 
48
        self._fileobj = fileobj
 
49
        self._compressor = bz2.BZ2Compressor()
 
50
 
 
51
    def _write_encoded(self, bytes):
 
52
        """Write bzip2-encoded bytes to the file"""
 
53
        self._fileobj.write(self._compressor.compress(bytes))
 
54
 
 
55
    def begin(self):
 
56
        """Start writing the bundle"""
 
57
        self._fileobj.write(serializer._get_bundle_header(
 
58
            serializer.v4_string))
 
59
        self._fileobj.write('#\n')
 
60
        self._container.begin()
 
61
 
 
62
    def end(self):
 
63
        """Finish writing the bundle"""
 
64
        self._container.end()
 
65
        self._fileobj.write(self._compressor.flush())
 
66
 
 
67
    def add_multiparent_record(self, mp_bytes, sha1, parents, repo_kind,
 
68
                               revision_id, file_id):
 
69
        """Add a record for a multi-parent diff
 
70
 
 
71
        :mp_bytes: A multi-parent diff, as a bytestring
 
72
        :sha1: The sha1 hash of the fulltext
 
73
        :parents: a list of revision-ids of the parents
 
74
        :repo_kind: The kind of object in the repository.  May be 'file' or
 
75
            'inventory'
 
76
        :revision_id: The revision id of the mpdiff being added.
 
77
        :file_id: The file-id of the file, or None for inventories.
 
78
        """
 
79
        metadata = {'parents': parents,
 
80
                    'storage_kind': 'mpdiff',
 
81
                    'sha1': sha1}
 
82
        self._add_record(mp_bytes, metadata, repo_kind, revision_id, file_id)
 
83
 
 
84
    def add_fulltext_record(self, bytes, parents, repo_kind, revision_id):
 
85
        """Add a record for a fulltext
 
86
 
 
87
        :bytes: The fulltext, as a bytestring
 
88
        :parents: a list of revision-ids of the parents
 
89
        :repo_kind: The kind of object in the repository.  May be 'revision' or
 
90
            'signature'
 
91
        :revision_id: The revision id of the fulltext being added.
 
92
        """
 
93
        metadata = {'parents': parents,
 
94
                    'storage_kind': 'mpdiff'}
 
95
        self._add_record(bytes, {'parents': parents,
 
96
            'storage_kind': 'fulltext'}, repo_kind, revision_id, None)
 
97
 
 
98
    def add_info_record(self, **kwargs):
 
99
        """Add an info record to the bundle
 
100
 
 
101
        Any parameters may be supplied, except 'self' and 'storage_kind'.
 
102
        Values must be lists, strings, integers, dicts, or a combination.
 
103
        """
 
104
        kwargs['storage_kind'] = 'header'
 
105
        self._add_record(None, kwargs, 'info', None, None)
 
106
 
 
107
    @staticmethod
 
108
    def encode_name(content_kind, revision_id, file_id=None):
 
109
        """Encode semantic ids as a container name"""
 
110
        if content_kind not in ('revision', 'file', 'inventory', 'signature',
 
111
                'info'):
 
112
            raise ValueError(content_kind)
 
113
        if content_kind == 'file':
 
114
            if file_id is None:
 
115
                raise AssertionError()
 
116
        else:
 
117
            if file_id is not None:
 
118
                raise AssertionError()
 
119
        if content_kind == 'info':
 
120
            if revision_id is not None:
 
121
                raise AssertionError()
 
122
        elif revision_id is None:
 
123
            raise AssertionError()
 
124
        names = [n.replace('/', '//') for n in
 
125
                 (content_kind, revision_id, file_id) if n is not None]
 
126
        return '/'.join(names)
 
127
 
 
128
    def _add_record(self, bytes, metadata, repo_kind, revision_id, file_id):
 
129
        """Add a bundle record to the container.
 
130
 
 
131
        Most bundle records are recorded as header/body pairs, with the
 
132
        body being nameless.  Records with storage_kind 'header' have no
 
133
        body.
 
134
        """
 
135
        name = self.encode_name(repo_kind, revision_id, file_id)
 
136
        encoded_metadata = bencode.bencode(metadata)
 
137
        self._container.add_bytes_record(encoded_metadata, [(name, )])
 
138
        if metadata['storage_kind'] != 'header':
 
139
            self._container.add_bytes_record(bytes, [])
 
140
 
 
141
 
 
142
class BundleReader(object):
 
143
    """Reader for bundle-format files.
 
144
 
 
145
    This serves roughly the same purpose as ContainerReader, but acts as a
 
146
    layer on top of it, providing metadata, a semantic name, and a record
 
147
    body
 
148
    """
 
149
 
 
150
    def __init__(self, fileobj, stream_input=True):
 
151
        """Constructor
 
152
 
 
153
        :param fileobj: a file containing a bzip-encoded container
 
154
        :param stream_input: If True, the BundleReader stream input rather than
 
155
            reading it all into memory at once.  Reading it into memory all at
 
156
            once is (currently) faster.
 
157
        """
 
158
        line = fileobj.readline()
 
159
        if line != '\n':
 
160
            fileobj.readline()
 
161
        self.patch_lines = []
 
162
        if stream_input:
 
163
            source_file = iterablefile.IterableFile(self.iter_decode(fileobj))
 
164
        else:
 
165
            source_file = StringIO(bz2.decompress(fileobj.read()))
 
166
        self._container_file = source_file
 
167
 
 
168
    @staticmethod
 
169
    def iter_decode(fileobj):
 
170
        """Iterate through decoded fragments of the file"""
 
171
        decompressor = bz2.BZ2Decompressor()
 
172
        for line in fileobj:
 
173
            try:
 
174
                yield decompressor.decompress(line)
 
175
            except EOFError:
 
176
                return
 
177
 
 
178
    @staticmethod
 
179
    def decode_name(name):
 
180
        """Decode a name from its container form into a semantic form
 
181
 
 
182
        :retval: content_kind, revision_id, file_id
 
183
        """
 
184
        segments = re.split('(//?)', name)
 
185
        names = ['']
 
186
        for segment in segments:
 
187
            if segment == '//':
 
188
                names[-1] += '/'
 
189
            elif segment == '/':
 
190
                names.append('')
 
191
            else:
 
192
                names[-1] += segment
 
193
        content_kind = names[0]
 
194
        revision_id = None
 
195
        file_id = None
 
196
        if len(names) > 1:
 
197
            revision_id = names[1]
 
198
        if len(names) > 2:
 
199
            file_id = names[2]
 
200
        return content_kind, revision_id, file_id
 
201
 
 
202
    def iter_records(self):
 
203
        """Iterate through bundle records
 
204
 
 
205
        :return: a generator of (bytes, metadata, content_kind, revision_id,
 
206
            file_id)
 
207
        """
 
208
        iterator = pack.iter_records_from_file(self._container_file)
 
209
        for names, bytes in iterator:
 
210
            if len(names) != 1:
 
211
                raise errors.BadBundle('Record has %d names instead of 1'
 
212
                                       % len(names))
 
213
            metadata = bencode.bdecode(bytes)
 
214
            if metadata['storage_kind'] == 'header':
 
215
                bytes = None
 
216
            else:
 
217
                _unused, bytes = iterator.next()
 
218
            yield (bytes, metadata) + self.decode_name(names[0][0])
 
219
 
 
220
 
 
221
class BundleSerializerV4(serializer.BundleSerializer):
 
222
    """Implement the high-level bundle interface"""
 
223
 
 
224
    def write(self, repository, revision_ids, forced_bases, fileobj):
 
225
        """Write a bundle to a file-like object
 
226
 
 
227
        For backwards-compatibility only
 
228
        """
 
229
        write_op = BundleWriteOperation.from_old_args(repository, revision_ids,
 
230
                                                      forced_bases, fileobj)
 
231
        return write_op.do_write()
 
232
 
 
233
    def write_bundle(self, repository, target, base, fileobj):
 
234
        """Write a bundle to a file object
 
235
 
 
236
        :param repository: The repository to retrieve revision data from
 
237
        :param target: The head revision to include ancestors of
 
238
        :param base: The ancestor of the target to stop including acestors
 
239
            at.
 
240
        :param fileobj: The file-like object to write to
 
241
        """
 
242
        write_op =  BundleWriteOperation(base, target, repository, fileobj)
 
243
        return write_op.do_write()
 
244
 
 
245
    def read(self, file):
 
246
        """return a reader object for a given file"""
 
247
        bundle = BundleInfoV4(file, self)
 
248
        return bundle
 
249
 
 
250
    @staticmethod
 
251
    def get_source_serializer(info):
 
252
        """Retrieve the serializer for a given info object"""
 
253
        return xml_serializer.format_registry.get(info['serializer'])
 
254
 
 
255
 
 
256
class BundleWriteOperation(object):
 
257
    """Perform the operation of writing revisions to a bundle"""
 
258
 
 
259
    @classmethod
 
260
    def from_old_args(cls, repository, revision_ids, forced_bases, fileobj):
 
261
        """Create a BundleWriteOperation from old-style arguments"""
 
262
        base, target = cls.get_base_target(revision_ids, forced_bases,
 
263
                                           repository)
 
264
        return BundleWriteOperation(base, target, repository, fileobj,
 
265
                                    revision_ids)
 
266
 
 
267
    def __init__(self, base, target, repository, fileobj, revision_ids=None):
 
268
        self.base = base
 
269
        self.target = target
 
270
        self.repository = repository
 
271
        bundle = BundleWriter(fileobj)
 
272
        self.bundle = bundle
 
273
        self.base_ancestry = set(repository.get_ancestry(base,
 
274
                                                         topo_sorted=False))
 
275
        if revision_ids is not None:
 
276
            self.revision_ids = revision_ids
 
277
        else:
 
278
            revision_ids = set(repository.get_ancestry(target,
 
279
                                                       topo_sorted=False))
 
280
            self.revision_ids = revision_ids.difference(self.base_ancestry)
 
281
        self.revision_keys = set([(revid,) for revid in self.revision_ids])
 
282
 
 
283
    def do_write(self):
 
284
        """Write all data to the bundle"""
 
285
        trace.note('Bundling %d revision(s).', len(self.revision_ids))
 
286
        self.repository.lock_read()
 
287
        try:
 
288
            self.bundle.begin()
 
289
            self.write_info()
 
290
            self.write_files()
 
291
            self.write_revisions()
 
292
            self.bundle.end()
 
293
        finally:
 
294
            self.repository.unlock()
 
295
        return self.revision_ids
 
296
 
 
297
    def write_info(self):
 
298
        """Write format info"""
 
299
        serializer_format = self.repository.get_serializer_format()
 
300
        supports_rich_root = {True: 1, False: 0}[
 
301
            self.repository.supports_rich_root()]
 
302
        self.bundle.add_info_record(serializer=serializer_format,
 
303
                                    supports_rich_root=supports_rich_root)
 
304
 
 
305
    def write_files(self):
 
306
        """Write bundle records for all revisions of all files"""
 
307
        text_keys = []
 
308
        altered_fileids = self.repository.fileids_altered_by_revision_ids(
 
309
                self.revision_ids)
 
310
        for file_id, revision_ids in altered_fileids.iteritems():
 
311
            for revision_id in revision_ids:
 
312
                text_keys.append((file_id, revision_id))
 
313
        self._add_mp_records_keys('file', self.repository.texts, text_keys)
 
314
 
 
315
    def write_revisions(self):
 
316
        """Write bundle records for all revisions and signatures"""
 
317
        inv_vf = self.repository.inventories
 
318
        revision_order = [key[-1] for key in multiparent.topo_iter_keys(inv_vf,
 
319
            self.revision_keys)]
 
320
        if self.target is not None and self.target in self.revision_ids:
 
321
            revision_order.remove(self.target)
 
322
            revision_order.append(self.target)
 
323
        self._add_mp_records_keys('inventory', inv_vf, [(revid,) for revid in revision_order])
 
324
        parent_map = self.repository.get_parent_map(revision_order)
 
325
        for revision_id in revision_order:
 
326
            parents = parent_map.get(revision_id, None)
 
327
            revision_text = self.repository.get_revision_xml(revision_id)
 
328
            self.bundle.add_fulltext_record(revision_text, parents,
 
329
                                       'revision', revision_id)
 
330
            try:
 
331
                self.bundle.add_fulltext_record(
 
332
                    self.repository.get_signature_text(
 
333
                    revision_id), parents, 'signature', revision_id)
 
334
            except errors.NoSuchRevision:
 
335
                pass
 
336
 
 
337
    @staticmethod
 
338
    def get_base_target(revision_ids, forced_bases, repository):
 
339
        """Determine the base and target from old-style revision ids"""
 
340
        if len(revision_ids) == 0:
 
341
            return None, None
 
342
        target = revision_ids[0]
 
343
        base = forced_bases.get(target)
 
344
        if base is None:
 
345
            parents = repository.get_revision(target).parent_ids
 
346
            if len(parents) == 0:
 
347
                base = _mod_revision.NULL_REVISION
 
348
            else:
 
349
                base = parents[0]
 
350
        return base, target
 
351
 
 
352
    def _add_mp_records_keys(self, repo_kind, vf, keys):
 
353
        """Add multi-parent diff records to a bundle"""
 
354
        ordered_keys = list(multiparent.topo_iter_keys(vf, keys))
 
355
        mpdiffs = vf.make_mpdiffs(ordered_keys)
 
356
        sha1s = vf.get_sha1s(ordered_keys)
 
357
        parent_map = vf.get_parent_map(ordered_keys)
 
358
        for mpdiff, item_key, in zip(mpdiffs, ordered_keys):
 
359
            sha1 = sha1s[item_key]
 
360
            parents = [key[-1] for key in parent_map[item_key]]
 
361
            text = ''.join(mpdiff.to_patch())
 
362
            # Infer file id records as appropriate.
 
363
            if len(item_key) == 2:
 
364
                file_id = item_key[0]
 
365
            else:
 
366
                file_id = None
 
367
            self.bundle.add_multiparent_record(text, sha1, parents, repo_kind,
 
368
                                               item_key[-1], file_id)
 
369
 
 
370
 
 
371
class BundleInfoV4(object):
 
372
 
 
373
    """Provide (most of) the BundleInfo interface"""
 
374
    def __init__(self, fileobj, serializer):
 
375
        self._fileobj = fileobj
 
376
        self._serializer = serializer
 
377
        self.__real_revisions = None
 
378
        self.__revisions = None
 
379
 
 
380
    def install(self, repository):
 
381
        return self.install_revisions(repository)
 
382
 
 
383
    def install_revisions(self, repository, stream_input=True):
 
384
        """Install this bundle's revisions into the specified repository
 
385
 
 
386
        :param target_repo: The repository to install into
 
387
        :param stream_input: If True, will stream input rather than reading it
 
388
            all into memory at once.  Reading it into memory all at once is
 
389
            (currently) faster.
 
390
        """
 
391
        repository.lock_write()
 
392
        try:
 
393
            ri = RevisionInstaller(self.get_bundle_reader(stream_input),
 
394
                                   self._serializer, repository)
 
395
            return ri.install()
 
396
        finally:
 
397
            repository.unlock()
 
398
 
 
399
    def get_merge_request(self, target_repo):
 
400
        """Provide data for performing a merge
 
401
 
 
402
        Returns suggested base, suggested target, and patch verification status
 
403
        """
 
404
        return None, self.target, 'inapplicable'
 
405
 
 
406
    def get_bundle_reader(self, stream_input=True):
 
407
        """Return a new BundleReader for the associated bundle
 
408
 
 
409
        :param stream_input: If True, the BundleReader stream input rather than
 
410
            reading it all into memory at once.  Reading it into memory all at
 
411
            once is (currently) faster.
 
412
        """
 
413
        self._fileobj.seek(0)
 
414
        return BundleReader(self._fileobj, stream_input)
 
415
 
 
416
    def _get_real_revisions(self):
 
417
        if self.__real_revisions is None:
 
418
            self.__real_revisions = []
 
419
            bundle_reader = self.get_bundle_reader()
 
420
            for bytes, metadata, repo_kind, revision_id, file_id in \
 
421
                bundle_reader.iter_records():
 
422
                if repo_kind == 'info':
 
423
                    serializer =\
 
424
                        self._serializer.get_source_serializer(metadata)
 
425
                if repo_kind == 'revision':
 
426
                    rev = serializer.read_revision_from_string(bytes)
 
427
                    self.__real_revisions.append(rev)
 
428
        return self.__real_revisions
 
429
    real_revisions = property(_get_real_revisions)
 
430
 
 
431
    def _get_revisions(self):
 
432
        if self.__revisions is None:
 
433
            self.__revisions = []
 
434
            for revision in self.real_revisions:
 
435
                self.__revisions.append(
 
436
                    bundle_data.RevisionInfo.from_revision(revision))
 
437
        return self.__revisions
 
438
 
 
439
    revisions = property(_get_revisions)
 
440
 
 
441
    def _get_target(self):
 
442
        return self.revisions[-1].revision_id
 
443
 
 
444
    target = property(_get_target)
 
445
 
 
446
 
 
447
class RevisionInstaller(object):
 
448
    """Installs revisions into a repository"""
 
449
 
 
450
    def __init__(self, container, serializer, repository):
 
451
        self._container = container
 
452
        self._serializer = serializer
 
453
        self._repository = repository
 
454
        self._info = None
 
455
 
 
456
    def install(self):
 
457
        """Perform the installation.
 
458
        
 
459
        Must be called with the Repository locked.
 
460
        """
 
461
        self._repository.start_write_group()
 
462
        try:
 
463
            result = self._install_in_write_group()
 
464
        except:
 
465
            self._repository.abort_write_group()
 
466
            raise
 
467
        self._repository.commit_write_group()
 
468
        return result
 
469
 
 
470
    def _install_in_write_group(self):
 
471
        current_file = None
 
472
        current_versionedfile = None
 
473
        pending_file_records = []
 
474
        inventory_vf = None
 
475
        pending_inventory_records = []
 
476
        added_inv = set()
 
477
        target_revision = None
 
478
        for bytes, metadata, repo_kind, revision_id, file_id in\
 
479
            self._container.iter_records():
 
480
            if repo_kind == 'info':
 
481
                if self._info is not None:
 
482
                    raise AssertionError()
 
483
                self._handle_info(metadata)
 
484
            if (pending_file_records and
 
485
                (repo_kind, file_id) != ('file', current_file)):
 
486
                # Flush the data for a single file - prevents memory
 
487
                # spiking due to buffering all files in memory.
 
488
                self._install_mp_records_keys(self._repository.texts,
 
489
                    pending_file_records)
 
490
                current_file = None
 
491
                del pending_file_records[:]
 
492
            if len(pending_inventory_records) > 0 and repo_kind != 'inventory':
 
493
                self._install_inventory_records(pending_inventory_records)
 
494
                pending_inventory_records = []
 
495
            if repo_kind == 'inventory':
 
496
                pending_inventory_records.append(((revision_id,), metadata, bytes))
 
497
            if repo_kind == 'revision':
 
498
                target_revision = revision_id
 
499
                self._install_revision(revision_id, metadata, bytes)
 
500
            if repo_kind == 'signature':
 
501
                self._install_signature(revision_id, metadata, bytes)
 
502
            if repo_kind == 'file':
 
503
                current_file = file_id
 
504
                pending_file_records.append(((file_id, revision_id), metadata, bytes))
 
505
        self._install_mp_records_keys(self._repository.texts, pending_file_records)
 
506
        return target_revision
 
507
 
 
508
    def _handle_info(self, info):
 
509
        """Extract data from an info record"""
 
510
        self._info = info
 
511
        self._source_serializer = self._serializer.get_source_serializer(info)
 
512
        if (info['supports_rich_root'] == 0 and
 
513
            self._repository.supports_rich_root()):
 
514
            self.update_root = True
 
515
        else:
 
516
            self.update_root = False
 
517
 
 
518
    def _install_mp_records(self, versionedfile, records):
 
519
        if len(records) == 0:
 
520
            return
 
521
        d_func = multiparent.MultiParent.from_patch
 
522
        vf_records = [(r, m['parents'], m['sha1'], d_func(t)) for r, m, t in
 
523
                      records if r not in versionedfile]
 
524
        versionedfile.add_mpdiffs(vf_records)
 
525
 
 
526
    def _install_mp_records_keys(self, versionedfile, records):
 
527
        d_func = multiparent.MultiParent.from_patch
 
528
        vf_records = []
 
529
        for key, meta, text in records:
 
530
            # Adapt to tuple interface: A length two key is a file_id,
 
531
            # revision_id pair, a length 1 key is a
 
532
            # revision/signature/inventory. We need to do this because
 
533
            # the metadata extraction from the bundle has not yet been updated
 
534
            # to use the consistent tuple interface itself.
 
535
            if len(key) == 2:
 
536
                prefix = key[:1]
 
537
            else:
 
538
                prefix = ()
 
539
            parents = [prefix + (parent,) for parent in meta['parents']]
 
540
            vf_records.append((key, parents, meta['sha1'], d_func(text)))
 
541
        versionedfile.add_mpdiffs(vf_records)
 
542
 
 
543
    def _install_inventory_records(self, records):
 
544
        if self._info['serializer'] == self._repository._serializer.format_num:
 
545
            return self._install_mp_records_keys(self._repository.inventories,
 
546
                records)
 
547
        for key, metadata, bytes in records:
 
548
            revision_id = key[-1]
 
549
            parent_ids = metadata['parents']
 
550
            parents = [self._repository.get_inventory(p)
 
551
                       for p in parent_ids]
 
552
            p_texts = [self._source_serializer.write_inventory_to_string(p)
 
553
                       for p in parents]
 
554
            target_lines = multiparent.MultiParent.from_patch(bytes).to_lines(
 
555
                p_texts)
 
556
            sha1 = osutils.sha_strings(target_lines)
 
557
            if sha1 != metadata['sha1']:
 
558
                raise errors.BadBundle("Can't convert to target format")
 
559
            target_inv = self._source_serializer.read_inventory_from_string(
 
560
                ''.join(target_lines))
 
561
            self._handle_root(target_inv, parent_ids)
 
562
            try:
 
563
                self._repository.add_inventory(revision_id, target_inv,
 
564
                                               parent_ids)
 
565
            except errors.UnsupportedInventoryKind:
 
566
                raise errors.IncompatibleRevision(repr(self._repository))
 
567
 
 
568
    def _handle_root(self, target_inv, parent_ids):
 
569
        revision_id = target_inv.revision_id
 
570
        if self.update_root:
 
571
            text_key = (target_inv.root.file_id, revision_id)
 
572
            parent_keys = [(target_inv.root.file_id, parent) for
 
573
                parent in parent_ids]
 
574
            self._repository.texts.add_lines(text_key, parent_keys, [])
 
575
        elif not self._repository.supports_rich_root():
 
576
            if target_inv.root.revision != revision_id:
 
577
                raise errors.IncompatibleRevision(repr(self._repository))
 
578
 
 
579
    def _install_revision(self, revision_id, metadata, text):
 
580
        if self._repository.has_revision(revision_id):
 
581
            return
 
582
        revision = self._source_serializer.read_revision_from_string(text)
 
583
        self._repository.add_revision(revision.revision_id, revision)
 
584
 
 
585
    def _install_signature(self, revision_id, metadata, text):
 
586
        transaction = self._repository.get_transaction()
 
587
        if self._repository.has_signature_for_revision_id(revision_id):
 
588
            return
 
589
        self._repository.add_signature_text(revision_id, text)