~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

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

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2008-10-31 04:39:04 UTC
  • mfrom: (3565.6.16 switch_nick)
  • Revision ID: pqm@pqm.ubuntu.com-20081031043904-52fnbfrloojemvcc
(mbp) branch nickname documentation

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005, 2006 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
 
 
17
"""Serializer factory for reading and writing bundles.
 
18
"""
 
19
 
 
20
import os
 
21
 
 
22
from bzrlib import errors
 
23
from bzrlib.bundle.serializer import (BundleSerializer,
 
24
                                      _get_bundle_header,
 
25
                                     )
 
26
from bzrlib.bundle.serializer import binary_diff
 
27
from bzrlib.bundle.bundle_data import (RevisionInfo, BundleInfo, BundleTree)
 
28
from bzrlib.diff import internal_diff
 
29
from bzrlib.osutils import pathjoin
 
30
from bzrlib.progress import DummyProgress
 
31
from bzrlib.revision import NULL_REVISION
 
32
import bzrlib.ui
 
33
from bzrlib.testament import StrictTestament
 
34
from bzrlib.timestamp import (
 
35
    format_highres_date,
 
36
    unpack_highres_date,
 
37
)
 
38
from bzrlib.textfile import text_file
 
39
from bzrlib.trace import mutter
 
40
 
 
41
bool_text = {True: 'yes', False: 'no'}
 
42
 
 
43
 
 
44
class Action(object):
 
45
    """Represent an action"""
 
46
 
 
47
    def __init__(self, name, parameters=None, properties=None):
 
48
        self.name = name
 
49
        if parameters is None:
 
50
            self.parameters = []
 
51
        else:
 
52
            self.parameters = parameters
 
53
        if properties is None:
 
54
            self.properties = []
 
55
        else:
 
56
            self.properties = properties
 
57
 
 
58
    def add_utf8_property(self, name, value):
 
59
        """Add a property whose value is currently utf8 to the action."""
 
60
        self.properties.append((name, value.decode('utf8')))
 
61
 
 
62
    def add_property(self, name, value):
 
63
        """Add a property to the action"""
 
64
        self.properties.append((name, value))
 
65
 
 
66
    def add_bool_property(self, name, value):
 
67
        """Add a boolean property to the action"""
 
68
        self.add_property(name, bool_text[value])
 
69
 
 
70
    def write(self, to_file):
 
71
        """Write action as to a file"""
 
72
        p_texts = [' '.join([self.name]+self.parameters)]
 
73
        for prop in self.properties:
 
74
            if len(prop) == 1:
 
75
                p_texts.append(prop[0])
 
76
            else:
 
77
                try:
 
78
                    p_texts.append('%s:%s' % prop)
 
79
                except:
 
80
                    raise repr(prop)
 
81
        text = ['=== ']
 
82
        text.append(' // '.join(p_texts))
 
83
        text_line = ''.join(text).encode('utf-8')
 
84
        available = 79
 
85
        while len(text_line) > available:
 
86
            to_file.write(text_line[:available])
 
87
            text_line = text_line[available:]
 
88
            to_file.write('\n... ')
 
89
            available = 79 - len('... ')
 
90
        to_file.write(text_line+'\n')
 
91
 
 
92
 
 
93
class BundleSerializerV08(BundleSerializer):
 
94
    def read(self, f):
 
95
        """Read the rest of the bundles from the supplied file.
 
96
 
 
97
        :param f: The file to read from
 
98
        :return: A list of bundles
 
99
        """
 
100
        return BundleReader(f).info
 
101
 
 
102
    def check_compatible(self):
 
103
        if self.source.supports_rich_root():
 
104
            raise errors.IncompatibleBundleFormat('0.8', repr(self.source))
 
105
 
 
106
    def write(self, source, revision_ids, forced_bases, f):
 
107
        """Write the bundless to the supplied files.
 
108
 
 
109
        :param source: A source for revision information
 
110
        :param revision_ids: The list of revision ids to serialize
 
111
        :param forced_bases: A dict of revision -> base that overrides default
 
112
        :param f: The file to output to
 
113
        """
 
114
        self.source = source
 
115
        self.revision_ids = revision_ids
 
116
        self.forced_bases = forced_bases
 
117
        self.to_file = f
 
118
        self.check_compatible()
 
119
        source.lock_read()
 
120
        try:
 
121
            self._write_main_header()
 
122
            pb = DummyProgress()
 
123
            try:
 
124
                self._write_revisions(pb)
 
125
            finally:
 
126
                pass
 
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 revsion 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()