/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: Aaron Bentley
  • Date: 2007-07-04 23:38:26 UTC
  • mto: This revision was merged to the branch mainline in revision 2631.
  • Revision ID: aaron.bentley@utoronto.ca-20070704233826-jkp63376wi1n96mm
update docs

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