~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-08-30 03:29:32 UTC
  • Revision ID: mbp@sourcefrog.net-20050830032932-1ad1c40cfacde866
- add a docstring for the placeholder plugin

- remove the checkperms shell plugin, which was in the wrong
  place anyhow

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