~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-06-22 09:35:24 UTC
  • Revision ID: mbp@sourcefrog.net-20050622093524-b15e2d374c2ae6ea
- move standard plugins from contrib/plugins to just ./plugins

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