~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/bundle/bundle_data.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2006-06-21 13:45:53 UTC
  • mfrom: (1793.2.6 bundles)
  • Revision ID: pqm@pqm.ubuntu.com-20060621134553-260d7058a7c44a27
Refactor bundle code

Show diffs side-by-side

added added

removed removed

Lines of Context:
21
21
import os
22
22
import pprint
23
23
 
24
 
from bzrlib.bundle.common import get_header, header_str
25
24
import bzrlib.errors
26
25
from bzrlib.errors import (TestamentMismatch, BzrError, 
27
26
                           MalformedHeader, MalformedPatches, NotABundle)
168
167
                return r
169
168
        raise KeyError(revision_id)
170
169
 
171
 
 
172
 
class BundleReader(object):
173
 
    """This class reads in a bundle from a file, and returns
174
 
    a Bundle object, which can then be applied against a tree.
175
 
    """
176
 
    def __init__(self, from_file):
177
 
        """Read in the bundle from the file.
178
 
 
179
 
        :param from_file: A file-like object (must have iterator support).
180
 
        """
181
 
        object.__init__(self)
182
 
        self.from_file = iter(from_file)
183
 
        self._next_line = None
184
 
        
185
 
        self.info = BundleInfo()
186
 
        # We put the actual inventory ids in the footer, so that the patch
187
 
        # is easier to read for humans.
188
 
        # Unfortunately, that means we need to read everything before we
189
 
        # can create a proper bundle.
190
 
        self._read()
191
 
        self._validate()
192
 
 
193
 
    def _read(self):
194
 
        self._read_header()
195
 
        while self._next_line is not None:
196
 
            self._read_revision_header()
197
 
            if self._next_line is None:
198
 
                break
199
 
            self._read_patches()
200
 
            self._read_footer()
201
 
 
202
 
    def _validate(self):
203
 
        """Make sure that the information read in makes sense
204
 
        and passes appropriate checksums.
205
 
        """
206
 
        # Fill in all the missing blanks for the revisions
207
 
        # and generate the real_revisions list.
208
 
        self.info.complete_info()
209
 
 
210
 
    def _validate_revision(self, inventory, revision_id):
211
 
        """Make sure all revision entries match their checksum."""
212
 
 
213
 
        # This is a mapping from each revision id to it's sha hash
214
 
        rev_to_sha1 = {}
215
 
        
216
 
        rev = self.info.get_revision(revision_id)
217
 
        rev_info = self.info.get_revision_info(revision_id)
218
 
        assert rev.revision_id == rev_info.revision_id
219
 
        assert rev.revision_id == revision_id
220
 
        sha1 = StrictTestament(rev, inventory).as_sha1()
221
 
        if sha1 != rev_info.sha1:
222
 
            raise TestamentMismatch(rev.revision_id, rev_info.sha1, sha1)
223
 
        if rev_to_sha1.has_key(rev.revision_id):
224
 
            raise BzrError('Revision {%s} given twice in the list'
225
 
                    % (rev.revision_id))
226
 
        rev_to_sha1[rev.revision_id] = sha1
 
170
    def revision_tree(self, repository, revision_id, base=None):
 
171
        revision = self.get_revision(revision_id)
 
172
        base = self.get_base(revision)
 
173
        assert base != revision_id
 
174
        self._validate_references_from_repository(repository)
 
175
        revision_info = self.get_revision_info(revision_id)
 
176
        inventory_revision_id = revision_id
 
177
        bundle_tree = BundleTree(repository.revision_tree(base), 
 
178
                                  inventory_revision_id)
 
179
        self._update_tree(bundle_tree, revision_id)
 
180
 
 
181
        inv = bundle_tree.inventory
 
182
        self._validate_inventory(inv, revision_id)
 
183
        self._validate_revision(inv, revision_id)
 
184
 
 
185
        return bundle_tree
227
186
 
228
187
    def _validate_references_from_repository(self, repository):
229
188
        """Now that we have a repository which should have some of the
251
210
        # All of the contained revisions were checked
252
211
        # in _validate_revisions
253
212
        checked = {}
254
 
        for rev_info in self.info.revisions:
 
213
        for rev_info in self.revisions:
255
214
            checked[rev_info.revision_id] = True
256
215
            add_sha(rev_to_sha, rev_info.revision_id, rev_info.sha1)
257
216
                
258
 
        for (rev, rev_info) in zip(self.info.real_revisions, self.info.revisions):
 
217
        for (rev, rev_info) in zip(self.real_revisions, self.revisions):
259
218
            add_sha(inv_to_sha, rev_info.revision_id, rev_info.inventory_sha1)
260
219
 
261
220
        count = 0
305
264
        s = serializer_v5.write_inventory_to_string(inv)
306
265
        sha1 = sha_string(s)
307
266
        # Target revision is the last entry in the real_revisions list
308
 
        rev = self.info.get_revision(revision_id)
 
267
        rev = self.get_revision(revision_id)
309
268
        assert rev.revision_id == revision_id
310
269
        if sha1 != rev.inventory_sha1:
311
270
            open(',,bogus-inv', 'wb').write(s)
312
271
            warning('Inventory sha hash mismatch for revision %s. %s'
313
272
                    ' != %s' % (revision_id, sha1, rev.inventory_sha1))
314
273
 
315
 
    def get_bundle(self, repository):
316
 
        """Return the meta information, and a Bundle tree which can
317
 
        be used to populate the local stores and working tree, respectively.
318
 
        """
319
 
        return self.info, self.revision_tree(repository, self.info.target)
320
 
 
321
 
    def revision_tree(self, repository, revision_id, base=None):
322
 
        revision = self.info.get_revision(revision_id)
323
 
        base = self.info.get_base(revision)
324
 
        assert base != revision_id
325
 
        self._validate_references_from_repository(repository)
326
 
        revision_info = self.info.get_revision_info(revision_id)
327
 
        inventory_revision_id = revision_id
328
 
        bundle_tree = BundleTree(repository.revision_tree(base), 
329
 
                                  inventory_revision_id)
330
 
        self._update_tree(bundle_tree, revision_id)
331
 
 
332
 
        inv = bundle_tree.inventory
333
 
        self._validate_inventory(inv, revision_id)
334
 
        self._validate_revision(inv, revision_id)
335
 
 
336
 
        return bundle_tree
337
 
 
338
 
    def _next(self):
339
 
        """yield the next line, but secretly
340
 
        keep 1 extra line for peeking.
341
 
        """
342
 
        for line in self.from_file:
343
 
            last = self._next_line
344
 
            self._next_line = line
345
 
            if last is not None:
346
 
                #mutter('yielding line: %r' % last)
347
 
                yield last
348
 
        last = self._next_line
349
 
        self._next_line = None
350
 
        #mutter('yielding line: %r' % last)
351
 
        yield last
352
 
 
353
 
    def _read_header(self):
354
 
        """Read the bzr header"""
355
 
        header = get_header()
356
 
        found = False
357
 
        for line in self._next():
358
 
            if found:
359
 
                # not all mailers will keep trailing whitespace
360
 
                if line == '#\n':
361
 
                    line = '# \n'
362
 
                if (not line.startswith('# ') or not line.endswith('\n')
363
 
                        or line[2:-1].decode('utf-8') != header[0]):
364
 
                    raise MalformedHeader('Found a header, but it'
365
 
                        ' was improperly formatted')
366
 
                header.pop(0) # We read this line.
367
 
                if not header:
368
 
                    break # We found everything.
369
 
            elif (line.startswith('#') and line.endswith('\n')):
370
 
                line = line[1:-1].strip().decode('utf-8')
371
 
                if line[:len(header_str)] == header_str:
372
 
                    if line == header[0]:
373
 
                        found = True
374
 
                    else:
375
 
                        raise MalformedHeader('Found what looks like'
376
 
                                ' a header, but did not match')
377
 
                    header.pop(0)
378
 
        else:
379
 
            raise NotABundle('Did not find an opening header')
380
 
 
381
 
    def _read_revision_header(self):
382
 
        self.info.revisions.append(RevisionInfo(None))
383
 
        for line in self._next():
384
 
            # The bzr header is terminated with a blank line
385
 
            # which does not start with '#'
386
 
            if line is None or line == '\n':
387
 
                break
388
 
            self._handle_next(line)
389
 
 
390
 
    def _read_next_entry(self, line, indent=1):
391
 
        """Read in a key-value pair
392
 
        """
393
 
        if not line.startswith('#'):
394
 
            raise MalformedHeader('Bzr header did not start with #')
395
 
        line = line[1:-1].decode('utf-8') # Remove the '#' and '\n'
396
 
        if line[:indent] == ' '*indent:
397
 
            line = line[indent:]
398
 
        if not line:
399
 
            return None, None# Ignore blank lines
400
 
 
401
 
        loc = line.find(': ')
402
 
        if loc != -1:
403
 
            key = line[:loc]
404
 
            value = line[loc+2:]
405
 
            if not value:
406
 
                value = self._read_many(indent=indent+2)
407
 
        elif line[-1:] == ':':
408
 
            key = line[:-1]
409
 
            value = self._read_many(indent=indent+2)
410
 
        else:
411
 
            raise MalformedHeader('While looking for key: value pairs,'
412
 
                    ' did not find the colon %r' % (line))
413
 
 
414
 
        key = key.replace(' ', '_')
415
 
        #mutter('found %s: %s' % (key, value))
416
 
        return key, value
417
 
 
418
 
    def _handle_next(self, line):
419
 
        if line is None:
420
 
            return
421
 
        key, value = self._read_next_entry(line, indent=1)
422
 
        mutter('_handle_next %r => %r' % (key, value))
423
 
        if key is None:
424
 
            return
425
 
 
426
 
        revision_info = self.info.revisions[-1]
427
 
        if hasattr(revision_info, key):
428
 
            if getattr(revision_info, key) is None:
429
 
                setattr(revision_info, key, value)
430
 
            else:
431
 
                raise MalformedHeader('Duplicated Key: %s' % key)
432
 
        else:
433
 
            # What do we do with a key we don't recognize
434
 
            raise MalformedHeader('Unknown Key: "%s"' % key)
435
 
    
436
 
    def _read_many(self, indent):
437
 
        """If a line ends with no entry, that means that it should be
438
 
        followed with multiple lines of values.
439
 
 
440
 
        This detects the end of the list, because it will be a line that
441
 
        does not start properly indented.
442
 
        """
443
 
        values = []
444
 
        start = '#' + (' '*indent)
445
 
 
446
 
        if self._next_line is None or self._next_line[:len(start)] != start:
447
 
            return values
448
 
 
449
 
        for line in self._next():
450
 
            values.append(line[len(start):-1].decode('utf-8'))
451
 
            if self._next_line is None or self._next_line[:len(start)] != start:
452
 
                break
453
 
        return values
454
 
 
455
 
    def _read_one_patch(self):
456
 
        """Read in one patch, return the complete patch, along with
457
 
        the next line.
458
 
 
459
 
        :return: action, lines, do_continue
460
 
        """
461
 
        #mutter('_read_one_patch: %r' % self._next_line)
462
 
        # Peek and see if there are no patches
463
 
        if self._next_line is None or self._next_line.startswith('#'):
464
 
            return None, [], False
465
 
 
466
 
        first = True
467
 
        lines = []
468
 
        for line in self._next():
469
 
            if first:
470
 
                if not line.startswith('==='):
471
 
                    raise MalformedPatches('The first line of all patches'
472
 
                        ' should be a bzr meta line "==="'
473
 
                        ': %r' % line)
474
 
                action = line[4:-1].decode('utf-8')
475
 
            elif line.startswith('... '):
476
 
                action += line[len('... '):-1].decode('utf-8')
477
 
 
478
 
            if (self._next_line is not None and 
479
 
                self._next_line.startswith('===')):
480
 
                return action, lines, True
481
 
            elif self._next_line is None or self._next_line.startswith('#'):
482
 
                return action, lines, False
483
 
 
484
 
            if first:
485
 
                first = False
486
 
            elif not line.startswith('... '):
487
 
                lines.append(line)
488
 
 
489
 
        return action, lines, False
490
 
            
491
 
    def _read_patches(self):
492
 
        do_continue = True
493
 
        revision_actions = []
494
 
        while do_continue:
495
 
            action, lines, do_continue = self._read_one_patch()
496
 
            if action is not None:
497
 
                revision_actions.append((action, lines))
498
 
        assert self.info.revisions[-1].tree_actions is None
499
 
        self.info.revisions[-1].tree_actions = revision_actions
500
 
 
501
 
    def _read_footer(self):
502
 
        """Read the rest of the meta information.
503
 
 
504
 
        :param first_line:  The previous step iterates past what it
505
 
                            can handle. That extra line is given here.
506
 
        """
507
 
        for line in self._next():
508
 
            self._handle_next(line)
509
 
            if not self._next_line.startswith('#'):
510
 
                self._next().next()
511
 
                break
512
 
            if self._next_line is None:
513
 
                break
 
274
    def _validate_revision(self, inventory, revision_id):
 
275
        """Make sure all revision entries match their checksum."""
 
276
 
 
277
        # This is a mapping from each revision id to it's sha hash
 
278
        rev_to_sha1 = {}
 
279
        
 
280
        rev = self.get_revision(revision_id)
 
281
        rev_info = self.get_revision_info(revision_id)
 
282
        assert rev.revision_id == rev_info.revision_id
 
283
        assert rev.revision_id == revision_id
 
284
        sha1 = StrictTestament(rev, inventory).as_sha1()
 
285
        if sha1 != rev_info.sha1:
 
286
            raise TestamentMismatch(rev.revision_id, rev_info.sha1, sha1)
 
287
        if rev_to_sha1.has_key(rev.revision_id):
 
288
            raise BzrError('Revision {%s} given twice in the list'
 
289
                    % (rev.revision_id))
 
290
        rev_to_sha1[rev.revision_id] = sha1
514
291
 
515
292
    def _update_tree(self, bundle_tree, revision_id):
516
293
        """This fills out a BundleTree based on the information
624
401
            'modified':modified
625
402
        }
626
403
        for action_line, lines in \
627
 
            self.info.get_revision_info(revision_id).tree_actions:
 
404
            self.get_revision_info(revision_id).tree_actions:
628
405
            first = action_line.find(' ')
629
406
            if first == -1:
630
407
                raise BzrError('Bogus action line'