~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

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

  • Committer: Keir Mierle
  • Date: 2006-11-23 18:56:25 UTC
  • mto: (2168.1.1 jam-integration)
  • mto: This revision was merged to the branch mainline in revision 2171.
  • Revision ID: keir@cs.utoronto.ca-20061123185625-ndto53ylcb8zo1y6
Fix spacing error and add tests for status --short command flag.

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