~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

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

  • Committer: Aaron Bentley
  • Date: 2007-06-20 22:06:22 UTC
  • mto: (2520.5.2 bzr.mpbundle)
  • mto: This revision was merged to the branch mainline in revision 2631.
  • Revision ID: abentley@panoramicfeedback.com-20070620220622-9lasxr96rr0e0xvn
Use a fresh versionedfile each time

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