~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/bundle/serializer/v08.py

  • Committer: Martin Pool
  • Date: 2005-06-27 03:03:00 UTC
  • mto: This revision was merged to the branch mainline in revision 852.
  • Revision ID: mbp@sourcefrog.net-20050627030300-19f4ca68fded6702
Add test for storing two text versions.

Store texts as (index, line) pairs, where versions include only a 
single index.  Filter out active lines from get method.

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