/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-03-13 23:24:13 UTC
  • mto: (7290.1.23 work)
  • mto: This revision was merged to the branch mainline in revision 7311.
  • Revision ID: jelmer@jelmer.uk-20190313232413-y1c951be4surcc9g
Fix formatting.

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