~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

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

merge merge tweaks from aaron, which includes latest .dev

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