~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

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

  • Committer: John Arbash Meinel
  • Date: 2005-09-15 21:35:53 UTC
  • mfrom: (907.1.57)
  • mto: (1393.2.1)
  • mto: This revision was merged to the branch mainline in revision 1396.
  • Revision ID: john@arbash-meinel.com-20050915213552-a6c83a5ef1e20897
(broken) Transport work is merged in. Tests do not pass yet.

Show diffs side-by-side

added added

removed removed

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