/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: 2017-11-11 13:10:32 UTC
  • mto: This revision was merged to the branch mainline in revision 6804.
  • Revision ID: jelmer@jelmer.uk-20171111131032-31lgi8qmvlz8363d
Fix typos.

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