~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-07-22 22:37:53 UTC
  • Revision ID: mbp@sourcefrog.net-20050722223753-7dced4e32d3ce21d
- add the start of a test for inventory file-id matching

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.delta import compare_trees
31
 
from bzrlib.diff import internal_diff
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
 
            if not self._read_revision_header():
333
 
                break
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 _next(self):
348
 
        """yield the next line, but secretly
349
 
        keep 1 extra line for peeking.
350
 
        """
351
 
        for line in self.from_file:
352
 
            last = self._next_line
353
 
            self._next_line = line
354
 
            if last is not None:
355
 
                #mutter('yielding line: %r' % last)
356
 
                yield last
357
 
        last = self._next_line
358
 
        self._next_line = None
359
 
        #mutter('yielding line: %r' % last)
360
 
        yield last
361
 
 
362
 
    def _read_revision_header(self):
363
 
        found_something = False
364
 
        self.info.revisions.append(RevisionInfo(None))
365
 
        for line in self._next():
366
 
            # The bzr header is terminated with a blank line
367
 
            # which does not start with '#'
368
 
            if line is None or line == '\n':
369
 
                break
370
 
            if not line.startswith('#'):
371
 
                continue
372
 
            found_something = True
373
 
            self._handle_next(line)
374
 
        if not found_something:
375
 
            # Nothing was there, so remove the added revision
376
 
            self.info.revisions.pop()
377
 
        return found_something
378
 
 
379
 
    def _read_next_entry(self, line, indent=1):
380
 
        """Read in a key-value pair
381
 
        """
382
 
        if not line.startswith('#'):
383
 
            raise errors.MalformedHeader('Bzr header did not start with #')
384
 
        line = line[1:-1].decode('utf-8') # Remove the '#' and '\n'
385
 
        if line[:indent] == ' '*indent:
386
 
            line = line[indent:]
387
 
        if not line:
388
 
            return None, None# Ignore blank lines
389
 
 
390
 
        loc = line.find(': ')
391
 
        if loc != -1:
392
 
            key = line[:loc]
393
 
            value = line[loc+2:]
394
 
            if not value:
395
 
                value = self._read_many(indent=indent+2)
396
 
        elif line[-1:] == ':':
397
 
            key = line[:-1]
398
 
            value = self._read_many(indent=indent+2)
399
 
        else:
400
 
            raise errors.MalformedHeader('While looking for key: value pairs,'
401
 
                    ' did not find the colon %r' % (line))
402
 
 
403
 
        key = key.replace(' ', '_')
404
 
        #mutter('found %s: %s' % (key, value))
405
 
        return key, value
406
 
 
407
 
    def _handle_next(self, line):
408
 
        if line is None:
409
 
            return
410
 
        key, value = self._read_next_entry(line, indent=1)
411
 
        mutter('_handle_next %r => %r' % (key, value))
412
 
        if key is None:
413
 
            return
414
 
 
415
 
        revision_info = self.info.revisions[-1]
416
 
        if hasattr(revision_info, key):
417
 
            if getattr(revision_info, key) is None:
418
 
                setattr(revision_info, key, value)
419
 
            else:
420
 
                raise errors.MalformedHeader('Duplicated Key: %s' % key)
421
 
        else:
422
 
            # What do we do with a key we don't recognize
423
 
            raise errors.MalformedHeader('Unknown Key: "%s"' % key)
424
 
    
425
 
    def _read_many(self, indent):
426
 
        """If a line ends with no entry, that means that it should be
427
 
        followed with multiple lines of values.
428
 
 
429
 
        This detects the end of the list, because it will be a line that
430
 
        does not start properly indented.
431
 
        """
432
 
        values = []
433
 
        start = '#' + (' '*indent)
434
 
 
435
 
        if self._next_line is None or self._next_line[:len(start)] != start:
436
 
            return values
437
 
 
438
 
        for line in self._next():
439
 
            values.append(line[len(start):-1].decode('utf-8'))
440
 
            if self._next_line is None or self._next_line[:len(start)] != start:
441
 
                break
442
 
        return values
443
 
 
444
 
    def _read_one_patch(self):
445
 
        """Read in one patch, return the complete patch, along with
446
 
        the next line.
447
 
 
448
 
        :return: action, lines, do_continue
449
 
        """
450
 
        #mutter('_read_one_patch: %r' % self._next_line)
451
 
        # Peek and see if there are no patches
452
 
        if self._next_line is None or self._next_line.startswith('#'):
453
 
            return None, [], False
454
 
 
455
 
        first = True
456
 
        lines = []
457
 
        for line in self._next():
458
 
            if first:
459
 
                if not line.startswith('==='):
460
 
                    raise errors.MalformedPatches('The first line of all patches'
461
 
                        ' should be a bzr meta line "==="'
462
 
                        ': %r' % line)
463
 
                action = line[4:-1].decode('utf-8')
464
 
            elif line.startswith('... '):
465
 
                action += line[len('... '):-1].decode('utf-8')
466
 
 
467
 
            if (self._next_line is not None and 
468
 
                self._next_line.startswith('===')):
469
 
                return action, lines, True
470
 
            elif self._next_line is None or self._next_line.startswith('#'):
471
 
                return action, lines, False
472
 
 
473
 
            if first:
474
 
                first = False
475
 
            elif not line.startswith('... '):
476
 
                lines.append(line)
477
 
 
478
 
        return action, lines, False
479
 
            
480
 
    def _read_patches(self):
481
 
        do_continue = True
482
 
        revision_actions = []
483
 
        while do_continue:
484
 
            action, lines, do_continue = self._read_one_patch()
485
 
            if action is not None:
486
 
                revision_actions.append((action, lines))
487
 
        assert self.info.revisions[-1].tree_actions is None
488
 
        self.info.revisions[-1].tree_actions = revision_actions
489
 
 
490
 
    def _read_footer(self):
491
 
        """Read the rest of the meta information.
492
 
 
493
 
        :param first_line:  The previous step iterates past what it
494
 
                            can handle. That extra line is given here.
495
 
        """
496
 
        for line in self._next():
497
 
            self._handle_next(line)
498
 
            if self._next_line is None:
499
 
                break
500
 
            if not self._next_line.startswith('#'):
501
 
                # Consume the trailing \n and stop processing
502
 
                self._next().next()
503
 
                break