/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/v07.py

  • Committer: Aaron Bentley
  • Date: 2006-06-20 02:32:24 UTC
  • mto: This revision was merged to the branch mainline in revision 1802.
  • Revision ID: aaron.bentley@utoronto.ca-20060620023224-745f7801d2ef3ac5
Move BundleReader into v07 serializer

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