~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

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

Exclude more files from dumb-rsync upload

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