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

  • Committer: Jelmer Vernooij
  • Date: 2020-05-05 23:32:39 UTC
  • mto: (7490.7.21 work)
  • mto: This revision was merged to the branch mainline in revision 7501.
  • Revision ID: jelmer@jelmer.uk-20200505233239-kdmnmscn8eisltk6
Add a breezy.__main__ module.

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 .... import (
 
23
    errors,
 
24
    ui,
 
25
    )
 
26
from . import (
 
27
    BundleSerializer,
 
28
    _get_bundle_header,
 
29
    binary_diff,
 
30
    )
 
31
from ..bundle_data import (
 
32
    RevisionInfo,
 
33
    BundleInfo,
 
34
    )
 
35
from ....diff import internal_diff
 
36
from ....revision import NULL_REVISION
 
37
from ....sixish import text_type
 
38
from ...testament import StrictTestament
 
39
from ....timestamp import (
 
40
    format_highres_date,
 
41
    )
 
42
from ....textfile import text_file
 
43
from ....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
                try:
 
273
                    tree_file = tree.get_file(path)
 
274
                except errors.NoSuchFile:
 
275
                    return []
 
276
                else:
 
277
                    if require_text is True:
 
278
                        tree_file = text_file(tree_file)
 
279
                    return tree_file.readlines()
 
280
 
 
281
            try:
 
282
                if force_binary:
 
283
                    raise errors.BinaryFile()
 
284
                old_lines = tree_lines(old_tree, old_path, require_text=True)
 
285
                new_lines = tree_lines(new_tree, new_path, require_text=True)
 
286
                action.write(self.to_file)
 
287
                internal_diff(old_path, old_lines, new_path, new_lines,
 
288
                              self.to_file)
 
289
            except errors.BinaryFile:
 
290
                old_lines = tree_lines(old_tree, old_path, require_text=False)
 
291
                new_lines = tree_lines(new_tree, new_path, require_text=False)
 
292
                action.add_property('encoding', 'base64')
 
293
                action.write(self.to_file)
 
294
                binary_diff(old_path, old_lines, new_path, new_lines,
 
295
                            self.to_file)
 
296
 
 
297
        def finish_action(action, file_id, kind, meta_modified, text_modified,
 
298
                          old_path, new_path):
 
299
            entry = new_tree.root_inventory.get_entry(file_id)
 
300
            if entry.revision != default_revision_id:
 
301
                action.add_utf8_property('last-changed', entry.revision)
 
302
            if meta_modified:
 
303
                action.add_bool_property('executable', entry.executable)
 
304
            if text_modified and kind == "symlink":
 
305
                action.add_property('target', entry.symlink_target)
 
306
            if text_modified and kind == "file":
 
307
                do_diff(file_id, old_path, new_path, action, force_binary)
 
308
            else:
 
309
                action.write(self.to_file)
 
310
 
 
311
        delta = new_tree.changes_from(old_tree, want_unchanged=True,
 
312
                                      include_root=True)
 
313
        for change in delta.removed:
 
314
            action = Action('removed', [change.kind[0], change.path[0]]).write(self.to_file)
 
315
 
 
316
        # TODO(jelmer): Treat copied specially here?
 
317
        for change in delta.added + delta.copied:
 
318
            action = Action(
 
319
                'added', [change.kind[1], change.path[1]],
 
320
                [('file-id', change.file_id.decode('utf-8'))])
 
321
            meta_modified = (change.kind[1] == 'file' and
 
322
                             change.executable[1])
 
323
            finish_action(action, change.file_id, change.kind[1], meta_modified, change.changed_content,
 
324
                          DEVNULL, change.path[1])
 
325
 
 
326
        for change in delta.renamed:
 
327
            action = Action('renamed', [change.kind[1], change.path[0]], [(change.path[1],)])
 
328
            finish_action(action, change.file_id, change.kind[1], change.meta_modified(), change.changed_content,
 
329
                          change.path[0], change.path[1])
 
330
 
 
331
        for change in delta.modified:
 
332
            action = Action('modified', [change.kind[1], change.path[1]])
 
333
            finish_action(action, change.file_id, change.kind[1], change.meta_modified(), change.changed_content,
 
334
                          change.path[0], change.path[1])
 
335
 
 
336
        for change in delta.unchanged:
 
337
            new_rev = new_tree.get_file_revision(change.path[1])
 
338
            if new_rev is None:
 
339
                continue
 
340
            old_rev = old_tree.get_file_revision(change.path[0])
 
341
            if new_rev != old_rev:
 
342
                action = Action('modified', [change.kind[1], change.path[1]])
 
343
                action.add_utf8_property('last-changed', new_rev)
 
344
                action.write(self.to_file)
 
345
 
 
346
 
 
347
class BundleReader(object):
 
348
    """This class reads in a bundle from a file, and returns
 
349
    a Bundle object, which can then be applied against a tree.
 
350
    """
 
351
 
 
352
    def __init__(self, from_file):
 
353
        """Read in the bundle from the file.
 
354
 
 
355
        :param from_file: A file-like object (must have iterator support).
 
356
        """
 
357
        object.__init__(self)
 
358
        self.from_file = iter(from_file)
 
359
        self._next_line = None
 
360
 
 
361
        self.info = self._get_info()
 
362
        # We put the actual inventory ids in the footer, so that the patch
 
363
        # is easier to read for humans.
 
364
        # Unfortunately, that means we need to read everything before we
 
365
        # can create a proper bundle.
 
366
        self._read()
 
367
        self._validate()
 
368
 
 
369
    def _get_info(self):
 
370
        return BundleInfo08()
 
371
 
 
372
    def _read(self):
 
373
        next(self._next())
 
374
        while self._next_line is not None:
 
375
            if not self._read_revision_header():
 
376
                break
 
377
            if self._next_line is None:
 
378
                break
 
379
            self._read_patches()
 
380
            self._read_footer()
 
381
 
 
382
    def _validate(self):
 
383
        """Make sure that the information read in makes sense
 
384
        and passes appropriate checksums.
 
385
        """
 
386
        # Fill in all the missing blanks for the revisions
 
387
        # and generate the real_revisions list.
 
388
        self.info.complete_info()
 
389
 
 
390
    def _next(self):
 
391
        """yield the next line, but secretly
 
392
        keep 1 extra line for peeking.
 
393
        """
 
394
        for line in self.from_file:
 
395
            last = self._next_line
 
396
            self._next_line = line
 
397
            if last is not None:
 
398
                #mutter('yielding line: %r' % last)
 
399
                yield last
 
400
        last = self._next_line
 
401
        self._next_line = None
 
402
        #mutter('yielding line: %r' % last)
 
403
        yield last
 
404
 
 
405
    def _read_revision_header(self):
 
406
        found_something = False
 
407
        self.info.revisions.append(RevisionInfo(None))
 
408
        for line in self._next():
 
409
            # The bzr header is terminated with a blank line
 
410
            # which does not start with '#'
 
411
            if line is None or line == b'\n':
 
412
                break
 
413
            if not line.startswith(b'#'):
 
414
                continue
 
415
            found_something = True
 
416
            self._handle_next(line)
 
417
        if not found_something:
 
418
            # Nothing was there, so remove the added revision
 
419
            self.info.revisions.pop()
 
420
        return found_something
 
421
 
 
422
    def _read_next_entry(self, line, indent=1):
 
423
        """Read in a key-value pair
 
424
        """
 
425
        if not line.startswith(b'#'):
 
426
            raise errors.MalformedHeader('Bzr header did not start with #')
 
427
        line = line[1:-1].decode('utf-8')  # Remove the '#' and '\n'
 
428
        if line[:indent] == ' ' * indent:
 
429
            line = line[indent:]
 
430
        if not line:
 
431
            return None, None  # Ignore blank lines
 
432
 
 
433
        loc = line.find(': ')
 
434
        if loc != -1:
 
435
            key = line[:loc]
 
436
            value = line[loc + 2:]
 
437
            if not value:
 
438
                value = self._read_many(indent=indent + 2)
 
439
        elif line[-1:] == ':':
 
440
            key = line[:-1]
 
441
            value = self._read_many(indent=indent + 2)
 
442
        else:
 
443
            raise errors.MalformedHeader('While looking for key: value pairs,'
 
444
                                         ' did not find the colon %r' % (line))
 
445
 
 
446
        key = key.replace(' ', '_')
 
447
        #mutter('found %s: %s' % (key, value))
 
448
        return key, value
 
449
 
 
450
    def _handle_next(self, line):
 
451
        if line is None:
 
452
            return
 
453
        key, value = self._read_next_entry(line, indent=1)
 
454
        mutter('_handle_next %r => %r' % (key, value))
 
455
        if key is None:
 
456
            return
 
457
 
 
458
        revision_info = self.info.revisions[-1]
 
459
        if key in revision_info.__dict__:
 
460
            if getattr(revision_info, key) is None:
 
461
                if key in ('file_id', 'revision_id', 'base_id'):
 
462
                    value = value.encode('utf8')
 
463
                elif key in ('parent_ids'):
 
464
                    value = [v.encode('utf8') for v in value]
 
465
                elif key in ('testament_sha1', 'inventory_sha1', 'sha1'):
 
466
                    value = value.encode('ascii')
 
467
                setattr(revision_info, key, value)
 
468
            else:
 
469
                raise errors.MalformedHeader('Duplicated Key: %s' % key)
 
470
        else:
 
471
            # What do we do with a key we don't recognize
 
472
            raise errors.MalformedHeader('Unknown Key: "%s"' % key)
 
473
 
 
474
    def _read_many(self, indent):
 
475
        """If a line ends with no entry, that means that it should be
 
476
        followed with multiple lines of values.
 
477
 
 
478
        This detects the end of the list, because it will be a line that
 
479
        does not start properly indented.
 
480
        """
 
481
        values = []
 
482
        start = b'#' + (b' ' * indent)
 
483
 
 
484
        if self._next_line is None or not self._next_line.startswith(start):
 
485
            return values
 
486
 
 
487
        for line in self._next():
 
488
            values.append(line[len(start):-1].decode('utf-8'))
 
489
            if self._next_line is None or not self._next_line.startswith(start):
 
490
                break
 
491
        return values
 
492
 
 
493
    def _read_one_patch(self):
 
494
        """Read in one patch, return the complete patch, along with
 
495
        the next line.
 
496
 
 
497
        :return: action, lines, do_continue
 
498
        """
 
499
        #mutter('_read_one_patch: %r' % self._next_line)
 
500
        # Peek and see if there are no patches
 
501
        if self._next_line is None or self._next_line.startswith(b'#'):
 
502
            return None, [], False
 
503
 
 
504
        first = True
 
505
        lines = []
 
506
        for line in self._next():
 
507
            if first:
 
508
                if not line.startswith(b'==='):
 
509
                    raise errors.MalformedPatches('The first line of all patches'
 
510
                                                  ' should be a bzr meta line "==="'
 
511
                                                  ': %r' % line)
 
512
                action = line[4:-1].decode('utf-8')
 
513
            elif line.startswith(b'... '):
 
514
                action += line[len(b'... '):-1].decode('utf-8')
 
515
 
 
516
            if (self._next_line is not None and
 
517
                    self._next_line.startswith(b'===')):
 
518
                return action, lines, True
 
519
            elif self._next_line is None or self._next_line.startswith(b'#'):
 
520
                return action, lines, False
 
521
 
 
522
            if first:
 
523
                first = False
 
524
            elif not line.startswith(b'... '):
 
525
                lines.append(line)
 
526
 
 
527
        return action, lines, False
 
528
 
 
529
    def _read_patches(self):
 
530
        do_continue = True
 
531
        revision_actions = []
 
532
        while do_continue:
 
533
            action, lines, do_continue = self._read_one_patch()
 
534
            if action is not None:
 
535
                revision_actions.append((action, lines))
 
536
        if self.info.revisions[-1].tree_actions is not None:
 
537
            raise AssertionError()
 
538
        self.info.revisions[-1].tree_actions = revision_actions
 
539
 
 
540
    def _read_footer(self):
 
541
        """Read the rest of the meta information.
 
542
 
 
543
        :param first_line:  The previous step iterates past what it
 
544
                            can handle. That extra line is given here.
 
545
        """
 
546
        for line in self._next():
 
547
            self._handle_next(line)
 
548
            if self._next_line is None:
 
549
                break
 
550
            if not self._next_line.startswith(b'#'):
 
551
                # Consume the trailing \n and stop processing
 
552
                next(self._next())
 
553
                break
 
554
 
 
555
 
 
556
class BundleInfo08(BundleInfo):
 
557
 
 
558
    def _update_tree(self, bundle_tree, revision_id):
 
559
        bundle_tree.note_last_changed('', revision_id)
 
560
        BundleInfo._update_tree(self, bundle_tree, revision_id)
 
561
 
 
562
    def _testament_sha1_from_revision(self, repository, revision_id):
 
563
        testament = StrictTestament.from_revision(repository, revision_id)
 
564
        return testament.as_sha1()
 
565
 
 
566
    def _testament(self, revision, tree):
 
567
        return StrictTestament(revision, tree)