~jelmer/ubuntu/maverick/bzr/proposed-2.2.3

« back to all changes in this revision

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

  • Committer: Bazaar Package Importer
  • Author(s): Steinar H. Gunderson
  • Date: 2006-11-08 01:28:55 UTC
  • mfrom: (1.1.15 upstream) (3.1.14 edgy)
  • Revision ID: james.westby@ubuntu.com-20061108012855-faugrhqdxr3cuu9o
Tags: 0.11-1.1
* Non-maintainer upload.
* In test_osutils.py, sort what we get from os.listdir() before checking it;
  the OS can give back directory entries in whatever order it likes, and
  this shouldn't break the tests. Fixes intermittent test failures and
  FTBFS. (Closes: #395203)

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
                rev_tree = self.source.revision_tree(rev_id)
 
162
            if rev_id in self.forced_bases:
 
163
                explicit_base = True
 
164
                base_id = self.forced_bases[rev_id]
 
165
                if base_id is None:
 
166
                    base_id = NULL_REVISION
 
167
            else:
 
168
                explicit_base = False
 
169
                if rev.parent_ids:
 
170
                    base_id = rev.parent_ids[-1]
 
171
                else:
 
172
                    base_id = NULL_REVISION
 
173
 
 
174
            if base_id == last_rev_id:
 
175
                base_tree = last_rev_tree
 
176
            else:
 
177
                base_tree = self.source.revision_tree(base_id)
 
178
            force_binary = (i != 0)
 
179
            self._write_revision(rev, rev_tree, base_id, base_tree, 
 
180
                                 explicit_base, force_binary)
 
181
 
 
182
            last_rev_id = base_id
 
183
            last_rev_tree = base_tree
 
184
 
 
185
    def _write_revision(self, rev, rev_tree, base_rev, base_tree, 
 
186
                        explicit_base, force_binary):
 
187
        """Write out the information for a revision."""
 
188
        def w(key, value):
 
189
            self._write(key, value, indent=1)
 
190
 
 
191
        w('message', rev.message.split('\n'))
 
192
        w('committer', rev.committer)
 
193
        w('date', format_highres_date(rev.timestamp, rev.timezone))
 
194
        self.to_file.write('\n')
 
195
 
 
196
        self._write_delta(rev_tree, base_tree, rev.revision_id, force_binary)
 
197
 
 
198
        w('revision id', rev.revision_id)
 
199
        w('sha1', StrictTestament.from_revision(self.source, 
 
200
                                                rev.revision_id).as_sha1())
 
201
        w('inventory sha1', rev.inventory_sha1)
 
202
        if rev.parent_ids:
 
203
            w('parent ids', rev.parent_ids)
 
204
        if explicit_base:
 
205
            w('base id', base_rev)
 
206
        if rev.properties:
 
207
            self._write('properties', None, indent=1)
 
208
            for name, value in rev.properties.items():
 
209
                self._write(name, value, indent=3)
 
210
        
 
211
        # Add an extra blank space at the end
 
212
        self.to_file.write('\n')
 
213
 
 
214
    def _write_action(self, name, parameters, properties=None):
 
215
        if properties is None:
 
216
            properties = []
 
217
        p_texts = ['%s:%s' % v for v in properties]
 
218
        self.to_file.write('=== ')
 
219
        self.to_file.write(' '.join([name]+parameters).encode('utf-8'))
 
220
        self.to_file.write(' // '.join(p_texts).encode('utf-8'))
 
221
        self.to_file.write('\n')
 
222
 
 
223
    def _write_delta(self, new_tree, old_tree, default_revision_id, 
 
224
                     force_binary):
 
225
        """Write out the changes between the trees."""
 
226
        DEVNULL = '/dev/null'
 
227
        old_label = ''
 
228
        new_label = ''
 
229
 
 
230
        def do_diff(file_id, old_path, new_path, action, force_binary):
 
231
            def tree_lines(tree, require_text=False):
 
232
                if file_id in tree:
 
233
                    tree_file = tree.get_file(file_id)
 
234
                    if require_text is True:
 
235
                        tree_file = text_file(tree_file)
 
236
                    return tree_file.readlines()
 
237
                else:
 
238
                    return []
 
239
 
 
240
            try:
 
241
                if force_binary:
 
242
                    raise errors.BinaryFile()
 
243
                old_lines = tree_lines(old_tree, require_text=True)
 
244
                new_lines = tree_lines(new_tree, require_text=True)
 
245
                action.write(self.to_file)
 
246
                internal_diff(old_path, old_lines, new_path, new_lines, 
 
247
                              self.to_file)
 
248
            except errors.BinaryFile:
 
249
                old_lines = tree_lines(old_tree, require_text=False)
 
250
                new_lines = tree_lines(new_tree, require_text=False)
 
251
                action.add_property('encoding', 'base64')
 
252
                action.write(self.to_file)
 
253
                binary_diff(old_path, old_lines, new_path, new_lines, 
 
254
                            self.to_file)
 
255
 
 
256
        def finish_action(action, file_id, kind, meta_modified, text_modified,
 
257
                          old_path, new_path):
 
258
            entry = new_tree.inventory[file_id]
 
259
            if entry.revision != default_revision_id:
 
260
                action.add_property('last-changed', entry.revision)
 
261
            if meta_modified:
 
262
                action.add_bool_property('executable', entry.executable)
 
263
            if text_modified and kind == "symlink":
 
264
                action.add_property('target', entry.symlink_target)
 
265
            if text_modified and kind == "file":
 
266
                do_diff(file_id, old_path, new_path, action, force_binary)
 
267
            else:
 
268
                action.write(self.to_file)
 
269
 
 
270
        delta = new_tree.changes_from(old_tree, want_unchanged=True)
 
271
        for path, file_id, kind in delta.removed:
 
272
            action = Action('removed', [kind, path]).write(self.to_file)
 
273
 
 
274
        for path, file_id, kind in delta.added:
 
275
            action = Action('added', [kind, path], [('file-id', file_id)])
 
276
            meta_modified = (kind=='file' and 
 
277
                             new_tree.is_executable(file_id))
 
278
            finish_action(action, file_id, kind, meta_modified, True,
 
279
                          DEVNULL, path)
 
280
 
 
281
        for (old_path, new_path, file_id, kind,
 
282
             text_modified, meta_modified) in delta.renamed:
 
283
            action = Action('renamed', [kind, old_path], [(new_path,)])
 
284
            finish_action(action, file_id, kind, meta_modified, text_modified,
 
285
                          old_path, new_path)
 
286
 
 
287
        for (path, file_id, kind,
 
288
             text_modified, meta_modified) in delta.modified:
 
289
            action = Action('modified', [kind, path])
 
290
            finish_action(action, file_id, kind, meta_modified, text_modified,
 
291
                          path, path)
 
292
 
 
293
        for path, file_id, kind in delta.unchanged:
 
294
            ie = new_tree.inventory[file_id]
 
295
            new_rev = getattr(ie, 'revision', None)
 
296
            if new_rev is None:
 
297
                continue
 
298
            old_rev = getattr(old_tree.inventory[ie.file_id], 'revision', None)
 
299
            if new_rev != old_rev:
 
300
                action = Action('modified', [ie.kind, 
 
301
                                             new_tree.id2path(ie.file_id)])
 
302
                action.add_property('last-changed', ie.revision)
 
303
                action.write(self.to_file)
 
304
 
 
305
 
 
306
class BundleReader(object):
 
307
    """This class reads in a bundle from a file, and returns
 
308
    a Bundle object, which can then be applied against a tree.
 
309
    """
 
310
    def __init__(self, from_file):
 
311
        """Read in the bundle from the file.
 
312
 
 
313
        :param from_file: A file-like object (must have iterator support).
 
314
        """
 
315
        object.__init__(self)
 
316
        self.from_file = iter(from_file)
 
317
        self._next_line = None
 
318
        
 
319
        self.info = BundleInfo08()
 
320
        # We put the actual inventory ids in the footer, so that the patch
 
321
        # is easier to read for humans.
 
322
        # Unfortunately, that means we need to read everything before we
 
323
        # can create a proper bundle.
 
324
        self._read()
 
325
        self._validate()
 
326
 
 
327
    def _read(self):
 
328
        self._next().next()
 
329
        while self._next_line is not None:
 
330
            if not self._read_revision_header():
 
331
                break
 
332
            if self._next_line is None:
 
333
                break
 
334
            self._read_patches()
 
335
            self._read_footer()
 
336
 
 
337
    def _validate(self):
 
338
        """Make sure that the information read in makes sense
 
339
        and passes appropriate checksums.
 
340
        """
 
341
        # Fill in all the missing blanks for the revisions
 
342
        # and generate the real_revisions list.
 
343
        self.info.complete_info()
 
344
 
 
345
    def _next(self):
 
346
        """yield the next line, but secretly
 
347
        keep 1 extra line for peeking.
 
348
        """
 
349
        for line in self.from_file:
 
350
            last = self._next_line
 
351
            self._next_line = line
 
352
            if last is not None:
 
353
                #mutter('yielding line: %r' % last)
 
354
                yield last
 
355
        last = self._next_line
 
356
        self._next_line = None
 
357
        #mutter('yielding line: %r' % last)
 
358
        yield last
 
359
 
 
360
    def _read_revision_header(self):
 
361
        found_something = False
 
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
            if not line.startswith('#'):
 
369
                continue
 
370
            found_something = True
 
371
            self._handle_next(line)
 
372
        if not found_something:
 
373
            # Nothing was there, so remove the added revision
 
374
            self.info.revisions.pop()
 
375
        return found_something
 
376
 
 
377
    def _read_next_entry(self, line, indent=1):
 
378
        """Read in a key-value pair
 
379
        """
 
380
        if not line.startswith('#'):
 
381
            raise errors.MalformedHeader('Bzr header did not start with #')
 
382
        line = line[1:-1].decode('utf-8') # Remove the '#' and '\n'
 
383
        if line[:indent] == ' '*indent:
 
384
            line = line[indent:]
 
385
        if not line:
 
386
            return None, None# Ignore blank lines
 
387
 
 
388
        loc = line.find(': ')
 
389
        if loc != -1:
 
390
            key = line[:loc]
 
391
            value = line[loc+2:]
 
392
            if not value:
 
393
                value = self._read_many(indent=indent+2)
 
394
        elif line[-1:] == ':':
 
395
            key = line[:-1]
 
396
            value = self._read_many(indent=indent+2)
 
397
        else:
 
398
            raise errors.MalformedHeader('While looking for key: value pairs,'
 
399
                    ' did not find the colon %r' % (line))
 
400
 
 
401
        key = key.replace(' ', '_')
 
402
        #mutter('found %s: %s' % (key, value))
 
403
        return key, value
 
404
 
 
405
    def _handle_next(self, line):
 
406
        if line is None:
 
407
            return
 
408
        key, value = self._read_next_entry(line, indent=1)
 
409
        mutter('_handle_next %r => %r' % (key, value))
 
410
        if key is None:
 
411
            return
 
412
 
 
413
        revision_info = self.info.revisions[-1]
 
414
        if key in revision_info.__dict__:
 
415
            if getattr(revision_info, key) is None:
 
416
                setattr(revision_info, key, value)
 
417
            else:
 
418
                raise errors.MalformedHeader('Duplicated Key: %s' % key)
 
419
        else:
 
420
            # What do we do with a key we don't recognize
 
421
            raise errors.MalformedHeader('Unknown Key: "%s"' % key)
 
422
    
 
423
    def _read_many(self, indent):
 
424
        """If a line ends with no entry, that means that it should be
 
425
        followed with multiple lines of values.
 
426
 
 
427
        This detects the end of the list, because it will be a line that
 
428
        does not start properly indented.
 
429
        """
 
430
        values = []
 
431
        start = '#' + (' '*indent)
 
432
 
 
433
        if self._next_line is None or self._next_line[:len(start)] != start:
 
434
            return values
 
435
 
 
436
        for line in self._next():
 
437
            values.append(line[len(start):-1].decode('utf-8'))
 
438
            if self._next_line is None or self._next_line[:len(start)] != start:
 
439
                break
 
440
        return values
 
441
 
 
442
    def _read_one_patch(self):
 
443
        """Read in one patch, return the complete patch, along with
 
444
        the next line.
 
445
 
 
446
        :return: action, lines, do_continue
 
447
        """
 
448
        #mutter('_read_one_patch: %r' % self._next_line)
 
449
        # Peek and see if there are no patches
 
450
        if self._next_line is None or self._next_line.startswith('#'):
 
451
            return None, [], False
 
452
 
 
453
        first = True
 
454
        lines = []
 
455
        for line in self._next():
 
456
            if first:
 
457
                if not line.startswith('==='):
 
458
                    raise errors.MalformedPatches('The first line of all patches'
 
459
                        ' should be a bzr meta line "==="'
 
460
                        ': %r' % line)
 
461
                action = line[4:-1].decode('utf-8')
 
462
            elif line.startswith('... '):
 
463
                action += line[len('... '):-1].decode('utf-8')
 
464
 
 
465
            if (self._next_line is not None and 
 
466
                self._next_line.startswith('===')):
 
467
                return action, lines, True
 
468
            elif self._next_line is None or self._next_line.startswith('#'):
 
469
                return action, lines, False
 
470
 
 
471
            if first:
 
472
                first = False
 
473
            elif not line.startswith('... '):
 
474
                lines.append(line)
 
475
 
 
476
        return action, lines, False
 
477
            
 
478
    def _read_patches(self):
 
479
        do_continue = True
 
480
        revision_actions = []
 
481
        while do_continue:
 
482
            action, lines, do_continue = self._read_one_patch()
 
483
            if action is not None:
 
484
                revision_actions.append((action, lines))
 
485
        assert self.info.revisions[-1].tree_actions is None
 
486
        self.info.revisions[-1].tree_actions = revision_actions
 
487
 
 
488
    def _read_footer(self):
 
489
        """Read the rest of the meta information.
 
490
 
 
491
        :param first_line:  The previous step iterates past what it
 
492
                            can handle. That extra line is given here.
 
493
        """
 
494
        for line in self._next():
 
495
            self._handle_next(line)
 
496
            if self._next_line is None:
 
497
                break
 
498
            if not self._next_line.startswith('#'):
 
499
                # Consume the trailing \n and stop processing
 
500
                self._next().next()
 
501
                break
 
502
 
 
503
 
 
504
class BundleInfo08(BundleInfo):
 
505
    def _update_tree(self, bundle_tree, revision_id):
 
506
        bundle_tree.note_last_changed('', revision_id)
 
507
        BundleInfo._update_tree(self, bundle_tree, revision_id)