/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-05-19 13:16:11 UTC
  • mto: (6968.4.3 git-archive)
  • mto: This revision was merged to the branch mainline in revision 6972.
  • Revision ID: jelmer@jelmer.uk-20180519131611-l9h9ud41j7qg1m03
Move tar/zip to breezy.archive.

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