~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-03-07 23:15:10 UTC
  • mto: (1551.19.24 Aaron's mergeable stuff)
  • mto: This revision was merged to the branch mainline in revision 2325.
  • Revision ID: abentley@panoramicfeedback.com-20070307231510-jae63zsli83db3eb
Make ChangeReporter private

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