~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

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

  • Committer: Aaron Bentley
  • Date: 2007-03-12 19:56:41 UTC
  • mto: (1551.19.24 Aaron's mergeable stuff)
  • mto: This revision was merged to the branch mainline in revision 2353.
  • Revision ID: abentley@panoramicfeedback.com-20070312195641-ezjnseqwgjtkh0iu
merge3 auto-detects line endings for conflict markers

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