~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

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

  • Committer: Alexander Belchenko
  • Date: 2007-04-14 12:17:31 UTC
  • mto: This revision was merged to the branch mainline in revision 2422.
  • Revision ID: bialix@ukr.net-20070414121731-jtc76rfulndihkh3
workingtree_implementations: make usage of symlinks optional

Show diffs side-by-side

added added

removed removed

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