/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

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

Merge bzr.dev

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 bundle_path(path):
 
231
            if path != '':
 
232
                return path
 
233
            else:
 
234
                return '.'
 
235
 
 
236
        def do_diff(file_id, old_path, new_path, action, force_binary):
 
237
            def tree_lines(tree, require_text=False):
 
238
                if file_id in tree:
 
239
                    tree_file = tree.get_file(file_id)
 
240
                    if require_text is True:
 
241
                        tree_file = text_file(tree_file)
 
242
                    return tree_file.readlines()
 
243
                else:
 
244
                    return []
 
245
 
 
246
            try:
 
247
                if force_binary:
 
248
                    raise errors.BinaryFile()
 
249
                old_lines = tree_lines(old_tree, require_text=True)
 
250
                new_lines = tree_lines(new_tree, require_text=True)
 
251
                action.write(self.to_file)
 
252
                internal_diff(old_path, old_lines, new_path, new_lines, 
 
253
                              self.to_file)
 
254
            except errors.BinaryFile:
 
255
                old_lines = tree_lines(old_tree, require_text=False)
 
256
                new_lines = tree_lines(new_tree, require_text=False)
 
257
                action.add_property('encoding', 'base64')
 
258
                action.write(self.to_file)
 
259
                binary_diff(old_path, old_lines, new_path, new_lines, 
 
260
                            self.to_file)
 
261
 
 
262
        def finish_action(action, file_id, kind, meta_modified, text_modified,
 
263
                          old_path, new_path):
 
264
            entry = new_tree.inventory[file_id]
 
265
            if (entry.revision != default_revision_id and 
 
266
                entry.revision is not None):
 
267
                action.add_property('last-changed', entry.revision)
 
268
            if meta_modified:
 
269
                action.add_bool_property('executable', entry.executable)
 
270
            if text_modified and kind == "symlink":
 
271
                action.add_property('target', entry.symlink_target)
 
272
            if text_modified and kind == "file":
 
273
                do_diff(file_id, old_path, new_path, action, force_binary)
 
274
            else:
 
275
                action.write(self.to_file)
 
276
 
 
277
        delta = new_tree.changes_from(old_tree, want_unchanged=True, 
 
278
                                      include_root=True)
 
279
        for path, file_id, kind in delta.removed:
 
280
            action = Action('removed', [kind, path]).write(self.to_file)
 
281
 
 
282
        for path, file_id, kind in delta.added:
 
283
            action = Action('added', [kind, bundle_path(path)], 
 
284
                            [('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, bundle_path(old_path)], 
 
293
                            [(bundle_path(new_path),)])
 
294
            finish_action(action, file_id, kind, meta_modified, text_modified,
 
295
                          old_path, new_path)
 
296
 
 
297
        for (path, file_id, kind,
 
298
             text_modified, meta_modified) in delta.modified:
 
299
            action = Action('modified', [kind, bundle_path(path)])
 
300
            finish_action(action, file_id, kind, meta_modified, text_modified,
 
301
                          path, path)
 
302
 
 
303
        for path, file_id, kind in delta.unchanged:
 
304
            ie = new_tree.inventory[file_id]
 
305
            new_rev = getattr(ie, 'revision', None)
 
306
            if new_rev is None:
 
307
                continue
 
308
            old_rev = getattr(old_tree.inventory[ie.file_id], 'revision', None)
 
309
            if new_rev != old_rev:
 
310
                action = Action('modified', 
 
311
                    [ie.kind, bundle_path(new_tree.id2path(ie.file_id))])
 
312
                if ie.revision is not None:
 
313
                    action.add_property('last-changed', ie.revision)
 
314
                action.write(self.to_file)
 
315
 
 
316
 
 
317
class BundleReader(object):
 
318
    """This class reads in a bundle from a file, and returns
 
319
    a Bundle object, which can then be applied against a tree.
 
320
    """
 
321
    def __init__(self, from_file):
 
322
        """Read in the bundle from the file.
 
323
 
 
324
        :param from_file: A file-like object (must have iterator support).
 
325
        """
 
326
        object.__init__(self)
 
327
        self.from_file = iter(from_file)
 
328
        self._next_line = None
 
329
        
 
330
        self.info = BundleInfo08()
 
331
        # We put the actual inventory ids in the footer, so that the patch
 
332
        # is easier to read for humans.
 
333
        # Unfortunately, that means we need to read everything before we
 
334
        # can create a proper bundle.
 
335
        self._read()
 
336
        self._validate()
 
337
 
 
338
    def _read(self):
 
339
        self._next().next()
 
340
        while self._next_line is not None:
 
341
            if not self._read_revision_header():
 
342
                break
 
343
            if self._next_line is None:
 
344
                break
 
345
            self._read_patches()
 
346
            self._read_footer()
 
347
 
 
348
    def _validate(self):
 
349
        """Make sure that the information read in makes sense
 
350
        and passes appropriate checksums.
 
351
        """
 
352
        # Fill in all the missing blanks for the revisions
 
353
        # and generate the real_revisions list.
 
354
        self.info.complete_info()
 
355
 
 
356
    def _next(self):
 
357
        """yield the next line, but secretly
 
358
        keep 1 extra line for peeking.
 
359
        """
 
360
        for line in self.from_file:
 
361
            last = self._next_line
 
362
            self._next_line = line
 
363
            if last is not None:
 
364
                #mutter('yielding line: %r' % last)
 
365
                yield last
 
366
        last = self._next_line
 
367
        self._next_line = None
 
368
        #mutter('yielding line: %r' % last)
 
369
        yield last
 
370
 
 
371
    def _read_revision_header(self):
 
372
        found_something = False
 
373
        self.info.revisions.append(RevisionInfo(None))
 
374
        for line in self._next():
 
375
            # The bzr header is terminated with a blank line
 
376
            # which does not start with '#'
 
377
            if line is None or line == '\n':
 
378
                break
 
379
            if not line.startswith('#'):
 
380
                continue
 
381
            found_something = True
 
382
            self._handle_next(line)
 
383
        if not found_something:
 
384
            # Nothing was there, so remove the added revision
 
385
            self.info.revisions.pop()
 
386
        return found_something
 
387
 
 
388
    def _read_next_entry(self, line, indent=1):
 
389
        """Read in a key-value pair
 
390
        """
 
391
        if not line.startswith('#'):
 
392
            raise errors.MalformedHeader('Bzr header did not start with #')
 
393
        line = line[1:-1].decode('utf-8') # Remove the '#' and '\n'
 
394
        if line[:indent] == ' '*indent:
 
395
            line = line[indent:]
 
396
        if not line:
 
397
            return None, None# Ignore blank lines
 
398
 
 
399
        loc = line.find(': ')
 
400
        if loc != -1:
 
401
            key = line[:loc]
 
402
            value = line[loc+2:]
 
403
            if not value:
 
404
                value = self._read_many(indent=indent+2)
 
405
        elif line[-1:] == ':':
 
406
            key = line[:-1]
 
407
            value = self._read_many(indent=indent+2)
 
408
        else:
 
409
            raise errors.MalformedHeader('While looking for key: value pairs,'
 
410
                    ' did not find the colon %r' % (line))
 
411
 
 
412
        key = key.replace(' ', '_')
 
413
        #mutter('found %s: %s' % (key, value))
 
414
        return key, value
 
415
 
 
416
    def _handle_next(self, line):
 
417
        if line is None:
 
418
            return
 
419
        key, value = self._read_next_entry(line, indent=1)
 
420
        mutter('_handle_next %r => %r' % (key, value))
 
421
        if key is None:
 
422
            return
 
423
 
 
424
        revision_info = self.info.revisions[-1]
 
425
        if key in revision_info.__dict__:
 
426
            if getattr(revision_info, key) is None:
 
427
                setattr(revision_info, key, value)
 
428
            else:
 
429
                raise errors.MalformedHeader('Duplicated Key: %s' % key)
 
430
        else:
 
431
            # What do we do with a key we don't recognize
 
432
            raise errors.MalformedHeader('Unknown Key: "%s"' % key)
 
433
    
 
434
    def _read_many(self, indent):
 
435
        """If a line ends with no entry, that means that it should be
 
436
        followed with multiple lines of values.
 
437
 
 
438
        This detects the end of the list, because it will be a line that
 
439
        does not start properly indented.
 
440
        """
 
441
        values = []
 
442
        start = '#' + (' '*indent)
 
443
 
 
444
        if self._next_line is None or self._next_line[:len(start)] != start:
 
445
            return values
 
446
 
 
447
        for line in self._next():
 
448
            values.append(line[len(start):-1].decode('utf-8'))
 
449
            if self._next_line is None or self._next_line[:len(start)] != start:
 
450
                break
 
451
        return values
 
452
 
 
453
    def _read_one_patch(self):
 
454
        """Read in one patch, return the complete patch, along with
 
455
        the next line.
 
456
 
 
457
        :return: action, lines, do_continue
 
458
        """
 
459
        #mutter('_read_one_patch: %r' % self._next_line)
 
460
        # Peek and see if there are no patches
 
461
        if self._next_line is None or self._next_line.startswith('#'):
 
462
            return None, [], False
 
463
 
 
464
        first = True
 
465
        lines = []
 
466
        for line in self._next():
 
467
            if first:
 
468
                if not line.startswith('==='):
 
469
                    raise errors.MalformedPatches('The first line of all patches'
 
470
                        ' should be a bzr meta line "==="'
 
471
                        ': %r' % line)
 
472
                action = line[4:-1].decode('utf-8')
 
473
            elif line.startswith('... '):
 
474
                action += line[len('... '):-1].decode('utf-8')
 
475
 
 
476
            if (self._next_line is not None and 
 
477
                self._next_line.startswith('===')):
 
478
                return action, lines, True
 
479
            elif self._next_line is None or self._next_line.startswith('#'):
 
480
                return action, lines, False
 
481
 
 
482
            if first:
 
483
                first = False
 
484
            elif not line.startswith('... '):
 
485
                lines.append(line)
 
486
 
 
487
        return action, lines, False
 
488
            
 
489
    def _read_patches(self):
 
490
        do_continue = True
 
491
        revision_actions = []
 
492
        while do_continue:
 
493
            action, lines, do_continue = self._read_one_patch()
 
494
            if action is not None:
 
495
                revision_actions.append((action, lines))
 
496
        assert self.info.revisions[-1].tree_actions is None
 
497
        self.info.revisions[-1].tree_actions = revision_actions
 
498
 
 
499
    def _read_footer(self):
 
500
        """Read the rest of the meta information.
 
501
 
 
502
        :param first_line:  The previous step iterates past what it
 
503
                            can handle. That extra line is given here.
 
504
        """
 
505
        for line in self._next():
 
506
            self._handle_next(line)
 
507
            if self._next_line is None:
 
508
                break
 
509
            if not self._next_line.startswith('#'):
 
510
                # Consume the trailing \n and stop processing
 
511
                self._next().next()
 
512
                break
 
513
 
 
514
 
 
515
class BundleInfo08(BundleInfo):
 
516
    def _update_tree(self, bundle_tree, revision_id):
 
517
        bundle_tree.note_last_changed('', revision_id)
 
518
        BundleInfo._update_tree(self, bundle_tree, revision_id)