/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-07-26 19:15:27 UTC
  • mto: This revision was merged to the branch mainline in revision 7055.
  • Revision ID: jelmer@jelmer.uk-20180726191527-wniq205k6tzfo1xx
Install fastimport from git.

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