~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

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

  • Committer: Martin Pool
  • Date: 2005-09-30 05:56:05 UTC
  • mto: (1185.14.2)
  • mto: This revision was merged to the branch mainline in revision 1396.
  • Revision ID: mbp@sourcefrog.net-20050930055605-a2c534529b392a7d
- fix upgrade for transport changes

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()