~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

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

  • Committer: Martin Pool
  • Date: 2005-09-16 07:03:16 UTC
  • Revision ID: mbp@sourcefrog.net-20050916070316-29f4b33b6f86fdc3
- cElementTree is typically not installed in util

  this was probably causing it to not be imported in many cases

Show diffs side-by-side

added added

removed removed

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