/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 breezy/bundle/serializer/v08.py

  • Committer: Jelmer Vernooij
  • Date: 2019-05-29 03:22:34 UTC
  • mfrom: (7303 work)
  • mto: This revision was merged to the branch mainline in revision 7306.
  • Revision ID: jelmer@jelmer.uk-20190529032234-mt3fuws8gq03tapi
Merge trunk.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005, 2006, 2009 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
"""Serializer factory for reading and writing bundles.
 
18
"""
 
19
 
 
20
from __future__ import absolute_import
 
21
 
 
22
from breezy import (
 
23
    errors,
 
24
    ui,
 
25
    )
 
26
from breezy.bundle.serializer import (
 
27
    BundleSerializer,
 
28
    _get_bundle_header,
 
29
    )
 
30
from breezy.bundle.serializer import binary_diff
 
31
from breezy.bundle.bundle_data import (
 
32
    RevisionInfo,
 
33
    BundleInfo,
 
34
    )
 
35
from breezy.diff import internal_diff
 
36
from breezy.revision import NULL_REVISION
 
37
from breezy.sixish import text_type
 
38
from breezy.bzr.testament import StrictTestament
 
39
from breezy.timestamp import (
 
40
    format_highres_date,
 
41
    )
 
42
from breezy.textfile import text_file
 
43
from breezy.trace import mutter
 
44
 
 
45
bool_text = {True: 'yes', False: 'no'}
 
46
 
 
47
 
 
48
class Action(object):
 
49
    """Represent an action"""
 
50
 
 
51
    def __init__(self, name, parameters=None, properties=None):
 
52
        self.name = name
 
53
        if parameters is None:
 
54
            self.parameters = []
 
55
        else:
 
56
            self.parameters = parameters
 
57
        if properties is None:
 
58
            self.properties = []
 
59
        else:
 
60
            self.properties = properties
 
61
 
 
62
    def add_utf8_property(self, name, value):
 
63
        """Add a property whose value is currently utf8 to the action."""
 
64
        self.properties.append((name, value.decode('utf8')))
 
65
 
 
66
    def add_property(self, name, value):
 
67
        """Add a property to the action"""
 
68
        self.properties.append((name, value))
 
69
 
 
70
    def add_bool_property(self, name, value):
 
71
        """Add a boolean property to the action"""
 
72
        self.add_property(name, bool_text[value])
 
73
 
 
74
    def write(self, to_file):
 
75
        """Write action as to a file"""
 
76
        p_texts = [' '.join([self.name] + self.parameters)]
 
77
        for prop in self.properties:
 
78
            if len(prop) == 1:
 
79
                p_texts.append(prop[0])
 
80
            else:
 
81
                p_texts.append('%s:%s' % prop)
 
82
        text = ['=== ']
 
83
        text.append(' // '.join(p_texts))
 
84
        text_line = ''.join(text).encode('utf-8')
 
85
        available = 79
 
86
        while len(text_line) > available:
 
87
            to_file.write(text_line[:available])
 
88
            text_line = text_line[available:]
 
89
            to_file.write(b'\n... ')
 
90
            available = 79 - len(b'... ')
 
91
        to_file.write(text_line + b'\n')
 
92
 
 
93
 
 
94
class BundleSerializerV08(BundleSerializer):
 
95
 
 
96
    def read(self, f):
 
97
        """Read the rest of the bundles from the supplied file.
 
98
 
 
99
        :param f: The file to read from
 
100
        :return: A list of bundles
 
101
        """
 
102
        return BundleReader(f).info
 
103
 
 
104
    def check_compatible(self):
 
105
        if self.source.supports_rich_root():
 
106
            raise errors.IncompatibleBundleFormat('0.8', repr(self.source))
 
107
 
 
108
    def write(self, source, revision_ids, forced_bases, f):
 
109
        """Write the bundless to the supplied files.
 
110
 
 
111
        :param source: A source for revision information
 
112
        :param revision_ids: The list of revision ids to serialize
 
113
        :param forced_bases: A dict of revision -> base that overrides default
 
114
        :param f: The file to output to
 
115
        """
 
116
        self.source = source
 
117
        self.revision_ids = revision_ids
 
118
        self.forced_bases = forced_bases
 
119
        self.to_file = f
 
120
        self.check_compatible()
 
121
        with source.lock_read():
 
122
            self._write_main_header()
 
123
            with ui.ui_factory.nested_progress_bar() as pb:
 
124
                self._write_revisions(pb)
 
125
 
 
126
    def write_bundle(self, repository, revision_id, base_revision_id, out):
 
127
        """Helper function for translating write_bundle to write"""
 
128
        forced_bases = {revision_id: base_revision_id}
 
129
        if base_revision_id is NULL_REVISION:
 
130
            base_revision_id = None
 
131
        graph = repository.get_graph()
 
132
        revision_ids = graph.find_unique_ancestors(revision_id,
 
133
                                                   [base_revision_id])
 
134
        revision_ids = list(repository.get_graph().iter_topo_order(
 
135
            revision_ids))
 
136
        revision_ids.reverse()
 
137
        self.write(repository, revision_ids, forced_bases, out)
 
138
        return revision_ids
 
139
 
 
140
    def _write_main_header(self):
 
141
        """Write the header for the changes"""
 
142
        f = self.to_file
 
143
        f.write(_get_bundle_header('0.8'))
 
144
        f.write(b'#\n')
 
145
 
 
146
    def _write(self, key, value, indent=1, trailing_space_when_empty=False):
 
147
        """Write out meta information, with proper indenting, etc.
 
148
 
 
149
        :param trailing_space_when_empty: To work around a bug in earlier
 
150
            bundle readers, when writing an empty property, we use "prop: \n"
 
151
            rather than writing "prop:\n".
 
152
            If this parameter is True, and value is the empty string, we will
 
153
            write an extra space.
 
154
        """
 
155
        if indent < 1:
 
156
            raise ValueError('indentation must be greater than 0')
 
157
        f = self.to_file
 
158
        f.write(b'#' + (b' ' * indent))
 
159
        f.write(key.encode('utf-8'))
 
160
        if not value:
 
161
            if trailing_space_when_empty and value == '':
 
162
                f.write(b': \n')
 
163
            else:
 
164
                f.write(b':\n')
 
165
        elif isinstance(value, bytes):
 
166
            f.write(b': ')
 
167
            f.write(value)
 
168
            f.write(b'\n')
 
169
        elif isinstance(value, text_type):
 
170
            f.write(b': ')
 
171
            f.write(value.encode('utf-8'))
 
172
            f.write(b'\n')
 
173
        else:
 
174
            f.write(b':\n')
 
175
            for entry in value:
 
176
                f.write(b'#' + (b' ' * (indent + 2)))
 
177
                if isinstance(entry, bytes):
 
178
                    f.write(entry)
 
179
                else:
 
180
                    f.write(entry.encode('utf-8'))
 
181
                f.write(b'\n')
 
182
 
 
183
    def _write_revisions(self, pb):
 
184
        """Write the information for all of the revisions."""
 
185
 
 
186
        # Optimize for the case of revisions in order
 
187
        last_rev_id = None
 
188
        last_rev_tree = None
 
189
 
 
190
        i_max = len(self.revision_ids)
 
191
        for i, rev_id in enumerate(self.revision_ids):
 
192
            pb.update("Generating revision data", i, i_max)
 
193
            rev = self.source.get_revision(rev_id)
 
194
            if rev_id == last_rev_id:
 
195
                rev_tree = last_rev_tree
 
196
            else:
 
197
                rev_tree = self.source.revision_tree(rev_id)
 
198
            if rev_id in self.forced_bases:
 
199
                explicit_base = True
 
200
                base_id = self.forced_bases[rev_id]
 
201
                if base_id is None:
 
202
                    base_id = NULL_REVISION
 
203
            else:
 
204
                explicit_base = False
 
205
                if rev.parent_ids:
 
206
                    base_id = rev.parent_ids[-1]
 
207
                else:
 
208
                    base_id = NULL_REVISION
 
209
 
 
210
            if base_id == last_rev_id:
 
211
                base_tree = last_rev_tree
 
212
            else:
 
213
                base_tree = self.source.revision_tree(base_id)
 
214
            force_binary = (i != 0)
 
215
            self._write_revision(rev, rev_tree, base_id, base_tree,
 
216
                                 explicit_base, force_binary)
 
217
 
 
218
            last_rev_id = base_id
 
219
            last_rev_tree = base_tree
 
220
 
 
221
    def _testament_sha1(self, revision_id):
 
222
        return StrictTestament.from_revision(self.source,
 
223
                                             revision_id).as_sha1()
 
224
 
 
225
    def _write_revision(self, rev, rev_tree, base_rev, base_tree,
 
226
                        explicit_base, force_binary):
 
227
        """Write out the information for a revision."""
 
228
        def w(key, value):
 
229
            self._write(key, value, indent=1)
 
230
 
 
231
        w('message', rev.message.split('\n'))
 
232
        w('committer', rev.committer)
 
233
        w('date', format_highres_date(rev.timestamp, rev.timezone))
 
234
        self.to_file.write(b'\n')
 
235
 
 
236
        self._write_delta(rev_tree, base_tree, rev.revision_id, force_binary)
 
237
 
 
238
        w('revision id', rev.revision_id)
 
239
        w('sha1', self._testament_sha1(rev.revision_id))
 
240
        w('inventory sha1', rev.inventory_sha1)
 
241
        if rev.parent_ids:
 
242
            w('parent ids', rev.parent_ids)
 
243
        if explicit_base:
 
244
            w('base id', base_rev)
 
245
        if rev.properties:
 
246
            self._write('properties', None, indent=1)
 
247
            for name, value in sorted(rev.properties.items()):
 
248
                self._write(name, value, indent=3,
 
249
                            trailing_space_when_empty=True)
 
250
 
 
251
        # Add an extra blank space at the end
 
252
        self.to_file.write(b'\n')
 
253
 
 
254
    def _write_action(self, name, parameters, properties=None):
 
255
        if properties is None:
 
256
            properties = []
 
257
        p_texts = ['%s:%s' % v for v in properties]
 
258
        self.to_file.write(b'=== ')
 
259
        self.to_file.write(' '.join([name] + parameters).encode('utf-8'))
 
260
        self.to_file.write(' // '.join(p_texts).encode('utf-8'))
 
261
        self.to_file.write(b'\n')
 
262
 
 
263
    def _write_delta(self, new_tree, old_tree, default_revision_id,
 
264
                     force_binary):
 
265
        """Write out the changes between the trees."""
 
266
        DEVNULL = '/dev/null'
 
267
        old_label = ''
 
268
        new_label = ''
 
269
 
 
270
        def do_diff(file_id, old_path, new_path, action, force_binary):
 
271
            def tree_lines(tree, path, require_text=False):
 
272
                if tree.has_id(file_id):
 
273
                    tree_file = tree.get_file(path)
 
274
                    if require_text is True:
 
275
                        tree_file = text_file(tree_file)
 
276
                    return tree_file.readlines()
 
277
                else:
 
278
                    return []
 
279
 
 
280
            try:
 
281
                if force_binary:
 
282
                    raise errors.BinaryFile()
 
283
                old_lines = tree_lines(old_tree, old_path, require_text=True)
 
284
                new_lines = tree_lines(new_tree, new_path, require_text=True)
 
285
                action.write(self.to_file)
 
286
                internal_diff(old_path, old_lines, new_path, new_lines,
 
287
                              self.to_file)
 
288
            except errors.BinaryFile:
 
289
                old_lines = tree_lines(old_tree, old_path, require_text=False)
 
290
                new_lines = tree_lines(new_tree, new_path, require_text=False)
 
291
                action.add_property('encoding', 'base64')
 
292
                action.write(self.to_file)
 
293
                binary_diff(old_path, old_lines, new_path, new_lines,
 
294
                            self.to_file)
 
295
 
 
296
        def finish_action(action, file_id, kind, meta_modified, text_modified,
 
297
                          old_path, new_path):
 
298
            entry = new_tree.root_inventory.get_entry(file_id)
 
299
            if entry.revision != default_revision_id:
 
300
                action.add_utf8_property('last-changed', entry.revision)
 
301
            if meta_modified:
 
302
                action.add_bool_property('executable', entry.executable)
 
303
            if text_modified and kind == "symlink":
 
304
                action.add_property('target', entry.symlink_target)
 
305
            if text_modified and kind == "file":
 
306
                do_diff(file_id, old_path, new_path, action, force_binary)
 
307
            else:
 
308
                action.write(self.to_file)
 
309
 
 
310
        delta = new_tree.changes_from(old_tree, want_unchanged=True,
 
311
                                      include_root=True)
 
312
        for path, file_id, kind in delta.removed:
 
313
            action = Action('removed', [kind, path]).write(self.to_file)
 
314
 
 
315
        for path, file_id, kind in delta.added:
 
316
            action = Action(
 
317
                'added', [kind, path], [('file-id', file_id.decode('utf-8'))])
 
318
            meta_modified = (kind == 'file' and
 
319
                             new_tree.is_executable(path))
 
320
            finish_action(action, file_id, kind, meta_modified, True,
 
321
                          DEVNULL, path)
 
322
 
 
323
        for (old_path, new_path, file_id, kind,
 
324
             text_modified, meta_modified) in delta.renamed:
 
325
            action = Action('renamed', [kind, old_path], [(new_path,)])
 
326
            finish_action(action, file_id, kind, meta_modified, text_modified,
 
327
                          old_path, new_path)
 
328
 
 
329
        for (path, file_id, kind,
 
330
             text_modified, meta_modified) in delta.modified:
 
331
            action = Action('modified', [kind, path])
 
332
            finish_action(action, file_id, kind, meta_modified, text_modified,
 
333
                          path, path)
 
334
 
 
335
        for path, file_id, kind in delta.unchanged:
 
336
            new_rev = new_tree.get_file_revision(path)
 
337
            if new_rev is None:
 
338
                continue
 
339
            old_rev = old_tree.get_file_revision(old_tree.id2path(file_id))
 
340
            if new_rev != old_rev:
 
341
                action = Action('modified', [new_tree.kind(path), path])
 
342
                action.add_utf8_property('last-changed', new_rev)
 
343
                action.write(self.to_file)
 
344
 
 
345
 
 
346
class BundleReader(object):
 
347
    """This class reads in a bundle from a file, and returns
 
348
    a Bundle object, which can then be applied against a tree.
 
349
    """
 
350
 
 
351
    def __init__(self, from_file):
 
352
        """Read in the bundle from the file.
 
353
 
 
354
        :param from_file: A file-like object (must have iterator support).
 
355
        """
 
356
        object.__init__(self)
 
357
        self.from_file = iter(from_file)
 
358
        self._next_line = None
 
359
 
 
360
        self.info = self._get_info()
 
361
        # We put the actual inventory ids in the footer, so that the patch
 
362
        # is easier to read for humans.
 
363
        # Unfortunately, that means we need to read everything before we
 
364
        # can create a proper bundle.
 
365
        self._read()
 
366
        self._validate()
 
367
 
 
368
    def _get_info(self):
 
369
        return BundleInfo08()
 
370
 
 
371
    def _read(self):
 
372
        next(self._next())
 
373
        while self._next_line is not None:
 
374
            if not self._read_revision_header():
 
375
                break
 
376
            if self._next_line is None:
 
377
                break
 
378
            self._read_patches()
 
379
            self._read_footer()
 
380
 
 
381
    def _validate(self):
 
382
        """Make sure that the information read in makes sense
 
383
        and passes appropriate checksums.
 
384
        """
 
385
        # Fill in all the missing blanks for the revisions
 
386
        # and generate the real_revisions list.
 
387
        self.info.complete_info()
 
388
 
 
389
    def _next(self):
 
390
        """yield the next line, but secretly
 
391
        keep 1 extra line for peeking.
 
392
        """
 
393
        for line in self.from_file:
 
394
            last = self._next_line
 
395
            self._next_line = line
 
396
            if last is not None:
 
397
                #mutter('yielding line: %r' % last)
 
398
                yield last
 
399
        last = self._next_line
 
400
        self._next_line = None
 
401
        #mutter('yielding line: %r' % last)
 
402
        yield last
 
403
 
 
404
    def _read_revision_header(self):
 
405
        found_something = False
 
406
        self.info.revisions.append(RevisionInfo(None))
 
407
        for line in self._next():
 
408
            # The bzr header is terminated with a blank line
 
409
            # which does not start with '#'
 
410
            if line is None or line == b'\n':
 
411
                break
 
412
            if not line.startswith(b'#'):
 
413
                continue
 
414
            found_something = True
 
415
            self._handle_next(line)
 
416
        if not found_something:
 
417
            # Nothing was there, so remove the added revision
 
418
            self.info.revisions.pop()
 
419
        return found_something
 
420
 
 
421
    def _read_next_entry(self, line, indent=1):
 
422
        """Read in a key-value pair
 
423
        """
 
424
        if not line.startswith(b'#'):
 
425
            raise errors.MalformedHeader('Bzr header did not start with #')
 
426
        line = line[1:-1].decode('utf-8')  # Remove the '#' and '\n'
 
427
        if line[:indent] == ' ' * indent:
 
428
            line = line[indent:]
 
429
        if not line:
 
430
            return None, None  # Ignore blank lines
 
431
 
 
432
        loc = line.find(': ')
 
433
        if loc != -1:
 
434
            key = line[:loc]
 
435
            value = line[loc + 2:]
 
436
            if not value:
 
437
                value = self._read_many(indent=indent + 2)
 
438
        elif line[-1:] == ':':
 
439
            key = line[:-1]
 
440
            value = self._read_many(indent=indent + 2)
 
441
        else:
 
442
            raise errors.MalformedHeader('While looking for key: value pairs,'
 
443
                                         ' did not find the colon %r' % (line))
 
444
 
 
445
        key = key.replace(' ', '_')
 
446
        #mutter('found %s: %s' % (key, value))
 
447
        return key, value
 
448
 
 
449
    def _handle_next(self, line):
 
450
        if line is None:
 
451
            return
 
452
        key, value = self._read_next_entry(line, indent=1)
 
453
        mutter('_handle_next %r => %r' % (key, value))
 
454
        if key is None:
 
455
            return
 
456
 
 
457
        revision_info = self.info.revisions[-1]
 
458
        if key in revision_info.__dict__:
 
459
            if getattr(revision_info, key) is None:
 
460
                if key in ('file_id', 'revision_id', 'base_id'):
 
461
                    value = value.encode('utf8')
 
462
                elif key in ('parent_ids'):
 
463
                    value = [v.encode('utf8') for v in value]
 
464
                elif key in ('testament_sha1', 'inventory_sha1', 'sha1'):
 
465
                    value = value.encode('ascii')
 
466
                setattr(revision_info, key, value)
 
467
            else:
 
468
                raise errors.MalformedHeader('Duplicated Key: %s' % key)
 
469
        else:
 
470
            # What do we do with a key we don't recognize
 
471
            raise errors.MalformedHeader('Unknown Key: "%s"' % key)
 
472
 
 
473
    def _read_many(self, indent):
 
474
        """If a line ends with no entry, that means that it should be
 
475
        followed with multiple lines of values.
 
476
 
 
477
        This detects the end of the list, because it will be a line that
 
478
        does not start properly indented.
 
479
        """
 
480
        values = []
 
481
        start = b'#' + (b' ' * indent)
 
482
 
 
483
        if self._next_line is None or not self._next_line.startswith(start):
 
484
            return values
 
485
 
 
486
        for line in self._next():
 
487
            values.append(line[len(start):-1].decode('utf-8'))
 
488
            if self._next_line is None or not self._next_line.startswith(start):
 
489
                break
 
490
        return values
 
491
 
 
492
    def _read_one_patch(self):
 
493
        """Read in one patch, return the complete patch, along with
 
494
        the next line.
 
495
 
 
496
        :return: action, lines, do_continue
 
497
        """
 
498
        #mutter('_read_one_patch: %r' % self._next_line)
 
499
        # Peek and see if there are no patches
 
500
        if self._next_line is None or self._next_line.startswith(b'#'):
 
501
            return None, [], False
 
502
 
 
503
        first = True
 
504
        lines = []
 
505
        for line in self._next():
 
506
            if first:
 
507
                if not line.startswith(b'==='):
 
508
                    raise errors.MalformedPatches('The first line of all patches'
 
509
                                                  ' should be a bzr meta line "==="'
 
510
                                                  ': %r' % line)
 
511
                action = line[4:-1].decode('utf-8')
 
512
            elif line.startswith(b'... '):
 
513
                action += line[len(b'... '):-1].decode('utf-8')
 
514
 
 
515
            if (self._next_line is not None and
 
516
                    self._next_line.startswith(b'===')):
 
517
                return action, lines, True
 
518
            elif self._next_line is None or self._next_line.startswith(b'#'):
 
519
                return action, lines, False
 
520
 
 
521
            if first:
 
522
                first = False
 
523
            elif not line.startswith(b'... '):
 
524
                lines.append(line)
 
525
 
 
526
        return action, lines, False
 
527
 
 
528
    def _read_patches(self):
 
529
        do_continue = True
 
530
        revision_actions = []
 
531
        while do_continue:
 
532
            action, lines, do_continue = self._read_one_patch()
 
533
            if action is not None:
 
534
                revision_actions.append((action, lines))
 
535
        if self.info.revisions[-1].tree_actions is not None:
 
536
            raise AssertionError()
 
537
        self.info.revisions[-1].tree_actions = revision_actions
 
538
 
 
539
    def _read_footer(self):
 
540
        """Read the rest of the meta information.
 
541
 
 
542
        :param first_line:  The previous step iterates past what it
 
543
                            can handle. That extra line is given here.
 
544
        """
 
545
        for line in self._next():
 
546
            self._handle_next(line)
 
547
            if self._next_line is None:
 
548
                break
 
549
            if not self._next_line.startswith(b'#'):
 
550
                # Consume the trailing \n and stop processing
 
551
                next(self._next())
 
552
                break
 
553
 
 
554
 
 
555
class BundleInfo08(BundleInfo):
 
556
 
 
557
    def _update_tree(self, bundle_tree, revision_id):
 
558
        bundle_tree.note_last_changed('', revision_id)
 
559
        BundleInfo._update_tree(self, bundle_tree, revision_id)
 
560
 
 
561
    def _testament_sha1_from_revision(self, repository, revision_id):
 
562
        testament = StrictTestament.from_revision(repository, revision_id)
 
563
        return testament.as_sha1()
 
564
 
 
565
    def _testament_sha1(self, revision, tree):
 
566
        return StrictTestament(revision, tree).as_sha1()