/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: 2018-11-16 11:42:27 UTC
  • mto: (7143.16.20 even-more-cleanups)
  • mto: This revision was merged to the branch mainline in revision 7175.
  • Revision ID: jelmer@jelmer.uk-20181116114227-lwabsodakoymo3ew
Remove flake8 issues now fixed.

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.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, file_id)
 
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('added', [kind, path], [
 
320
                            ('file-id', file_id.decode('utf-8'))])
 
321
            meta_modified = (kind == 'file' and
 
322
                             new_tree.is_executable(path, file_id))
 
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, file_id)
 
340
            if new_rev is None:
 
341
                continue
 
342
            old_rev = old_tree.get_file_revision(
 
343
                old_tree.id2path(file_id), file_id)
 
344
            if new_rev != old_rev:
 
345
                action = Action('modified', [new_tree.kind(path, file_id),
 
346
                                             path])
 
347
                action.add_utf8_property('last-changed', new_rev)
 
348
                action.write(self.to_file)
 
349
 
 
350
 
 
351
class BundleReader(object):
 
352
    """This class reads in a bundle from a file, and returns
 
353
    a Bundle object, which can then be applied against a tree.
 
354
    """
 
355
 
 
356
    def __init__(self, from_file):
 
357
        """Read in the bundle from the file.
 
358
 
 
359
        :param from_file: A file-like object (must have iterator support).
 
360
        """
 
361
        object.__init__(self)
 
362
        self.from_file = iter(from_file)
 
363
        self._next_line = None
 
364
 
 
365
        self.info = self._get_info()
 
366
        # We put the actual inventory ids in the footer, so that the patch
 
367
        # is easier to read for humans.
 
368
        # Unfortunately, that means we need to read everything before we
 
369
        # can create a proper bundle.
 
370
        self._read()
 
371
        self._validate()
 
372
 
 
373
    def _get_info(self):
 
374
        return BundleInfo08()
 
375
 
 
376
    def _read(self):
 
377
        next(self._next())
 
378
        while self._next_line is not None:
 
379
            if not self._read_revision_header():
 
380
                break
 
381
            if self._next_line is None:
 
382
                break
 
383
            self._read_patches()
 
384
            self._read_footer()
 
385
 
 
386
    def _validate(self):
 
387
        """Make sure that the information read in makes sense
 
388
        and passes appropriate checksums.
 
389
        """
 
390
        # Fill in all the missing blanks for the revisions
 
391
        # and generate the real_revisions list.
 
392
        self.info.complete_info()
 
393
 
 
394
    def _next(self):
 
395
        """yield the next line, but secretly
 
396
        keep 1 extra line for peeking.
 
397
        """
 
398
        for line in self.from_file:
 
399
            last = self._next_line
 
400
            self._next_line = line
 
401
            if last is not None:
 
402
                #mutter('yielding line: %r' % last)
 
403
                yield last
 
404
        last = self._next_line
 
405
        self._next_line = None
 
406
        #mutter('yielding line: %r' % last)
 
407
        yield last
 
408
 
 
409
    def _read_revision_header(self):
 
410
        found_something = False
 
411
        self.info.revisions.append(RevisionInfo(None))
 
412
        for line in self._next():
 
413
            # The bzr header is terminated with a blank line
 
414
            # which does not start with '#'
 
415
            if line is None or line == b'\n':
 
416
                break
 
417
            if not line.startswith(b'#'):
 
418
                continue
 
419
            found_something = True
 
420
            self._handle_next(line)
 
421
        if not found_something:
 
422
            # Nothing was there, so remove the added revision
 
423
            self.info.revisions.pop()
 
424
        return found_something
 
425
 
 
426
    def _read_next_entry(self, line, indent=1):
 
427
        """Read in a key-value pair
 
428
        """
 
429
        if not line.startswith(b'#'):
 
430
            raise errors.MalformedHeader('Bzr header did not start with #')
 
431
        line = line[1:-1].decode('utf-8')  # Remove the '#' and '\n'
 
432
        if line[:indent] == ' ' * indent:
 
433
            line = line[indent:]
 
434
        if not line:
 
435
            return None, None  # Ignore blank lines
 
436
 
 
437
        loc = line.find(': ')
 
438
        if loc != -1:
 
439
            key = line[:loc]
 
440
            value = line[loc + 2:]
 
441
            if not value:
 
442
                value = self._read_many(indent=indent + 2)
 
443
        elif line[-1:] == ':':
 
444
            key = line[:-1]
 
445
            value = self._read_many(indent=indent + 2)
 
446
        else:
 
447
            raise errors.MalformedHeader('While looking for key: value pairs,'
 
448
                                         ' did not find the colon %r' % (line))
 
449
 
 
450
        key = key.replace(' ', '_')
 
451
        #mutter('found %s: %s' % (key, value))
 
452
        return key, value
 
453
 
 
454
    def _handle_next(self, line):
 
455
        if line is None:
 
456
            return
 
457
        key, value = self._read_next_entry(line, indent=1)
 
458
        mutter('_handle_next %r => %r' % (key, value))
 
459
        if key is None:
 
460
            return
 
461
 
 
462
        revision_info = self.info.revisions[-1]
 
463
        if key in revision_info.__dict__:
 
464
            if getattr(revision_info, key) is None:
 
465
                if key in ('file_id', 'revision_id', 'base_id'):
 
466
                    value = value.encode('utf8')
 
467
                elif key in ('parent_ids'):
 
468
                    value = [v.encode('utf8') for v in value]
 
469
                elif key in ('testament_sha1', 'inventory_sha1', 'sha1'):
 
470
                    value = value.encode('ascii')
 
471
                setattr(revision_info, key, value)
 
472
            else:
 
473
                raise errors.MalformedHeader('Duplicated Key: %s' % key)
 
474
        else:
 
475
            # What do we do with a key we don't recognize
 
476
            raise errors.MalformedHeader('Unknown Key: "%s"' % key)
 
477
 
 
478
    def _read_many(self, indent):
 
479
        """If a line ends with no entry, that means that it should be
 
480
        followed with multiple lines of values.
 
481
 
 
482
        This detects the end of the list, because it will be a line that
 
483
        does not start properly indented.
 
484
        """
 
485
        values = []
 
486
        start = b'#' + (b' ' * indent)
 
487
 
 
488
        if self._next_line is None or not self._next_line.startswith(start):
 
489
            return values
 
490
 
 
491
        for line in self._next():
 
492
            values.append(line[len(start):-1].decode('utf-8'))
 
493
            if self._next_line is None or not self._next_line.startswith(start):
 
494
                break
 
495
        return values
 
496
 
 
497
    def _read_one_patch(self):
 
498
        """Read in one patch, return the complete patch, along with
 
499
        the next line.
 
500
 
 
501
        :return: action, lines, do_continue
 
502
        """
 
503
        #mutter('_read_one_patch: %r' % self._next_line)
 
504
        # Peek and see if there are no patches
 
505
        if self._next_line is None or self._next_line.startswith(b'#'):
 
506
            return None, [], False
 
507
 
 
508
        first = True
 
509
        lines = []
 
510
        for line in self._next():
 
511
            if first:
 
512
                if not line.startswith(b'==='):
 
513
                    raise errors.MalformedPatches('The first line of all patches'
 
514
                                                  ' should be a bzr meta line "==="'
 
515
                                                  ': %r' % line)
 
516
                action = line[4:-1].decode('utf-8')
 
517
            elif line.startswith(b'... '):
 
518
                action += line[len(b'... '):-1].decode('utf-8')
 
519
 
 
520
            if (self._next_line is not None and
 
521
                    self._next_line.startswith(b'===')):
 
522
                return action, lines, True
 
523
            elif self._next_line is None or self._next_line.startswith(b'#'):
 
524
                return action, lines, False
 
525
 
 
526
            if first:
 
527
                first = False
 
528
            elif not line.startswith(b'... '):
 
529
                lines.append(line)
 
530
 
 
531
        return action, lines, False
 
532
 
 
533
    def _read_patches(self):
 
534
        do_continue = True
 
535
        revision_actions = []
 
536
        while do_continue:
 
537
            action, lines, do_continue = self._read_one_patch()
 
538
            if action is not None:
 
539
                revision_actions.append((action, lines))
 
540
        if self.info.revisions[-1].tree_actions is not None:
 
541
            raise AssertionError()
 
542
        self.info.revisions[-1].tree_actions = revision_actions
 
543
 
 
544
    def _read_footer(self):
 
545
        """Read the rest of the meta information.
 
546
 
 
547
        :param first_line:  The previous step iterates past what it
 
548
                            can handle. That extra line is given here.
 
549
        """
 
550
        for line in self._next():
 
551
            self._handle_next(line)
 
552
            if self._next_line is None:
 
553
                break
 
554
            if not self._next_line.startswith(b'#'):
 
555
                # Consume the trailing \n and stop processing
 
556
                next(self._next())
 
557
                break
 
558
 
 
559
 
 
560
class BundleInfo08(BundleInfo):
 
561
 
 
562
    def _update_tree(self, bundle_tree, revision_id):
 
563
        bundle_tree.note_last_changed('', revision_id)
 
564
        BundleInfo._update_tree(self, bundle_tree, revision_id)
 
565
 
 
566
    def _testament_sha1_from_revision(self, repository, revision_id):
 
567
        testament = StrictTestament.from_revision(repository, revision_id)
 
568
        return testament.as_sha1()
 
569
 
 
570
    def _testament_sha1(self, revision, tree):
 
571
        return StrictTestament(revision, tree).as_sha1()