~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

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

  • Committer: Jelmer Vernooij
  • Date: 2011-12-19 13:23:58 UTC
  • mto: This revision was merged to the branch mainline in revision 6386.
  • Revision ID: jelmer@canonical.com-20111219132358-uvs5a6y92gomzacd
Move importing from future until after doc string, otherwise the doc string will disappear.

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