~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

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

  • Committer: Alexander Belchenko
  • Date: 2006-07-31 16:12:57 UTC
  • mto: (1711.2.111 jam-integration)
  • mto: This revision was merged to the branch mainline in revision 1906.
  • Revision ID: bialix@ukr.net-20060731161257-91a231523255332c
new official bzr.ico

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