/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

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2006-06-22 18:37:26 UTC
  • mfrom: (1551.7.5 Aaron's mergeable stuff)
  • Revision ID: pqm@pqm.ubuntu.com-20060622183726-70f1e7cb560cf090
Update StrictTestament support and as_sha1 algorithm, bump bundle version

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