~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

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

  • Committer: Martin Pool
  • Date: 2007-07-11 04:15:48 UTC
  • mto: This revision was merged to the branch mainline in revision 2605.
  • Revision ID: mbp@sourcefrog.net-20070711041548-n1om2ptyj2j01r6l
Clean up old/unused global options

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