~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/bundle/read_bundle.py

Move doctest import to increase speed

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
24
25
import bzrlib.errors
25
26
from bzrlib.errors import (TestamentMismatch, BzrError, 
26
27
                           MalformedHeader, MalformedPatches, NotABundle)
27
28
from bzrlib.inventory import (Inventory, InventoryEntry,
28
29
                              InventoryDirectory, InventoryFile,
29
30
                              InventoryLink)
30
 
from bzrlib.osutils import sha_file, sha_string, pathjoin
 
31
from bzrlib.osutils import sha_file, sha_string
31
32
from bzrlib.revision import Revision, NULL_REVISION
32
33
from bzrlib.testament import StrictTestament
33
34
from bzrlib.trace import mutter, warning
167
168
                return r
168
169
        raise KeyError(revision_id)
169
170
 
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
 
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
186
227
 
187
228
    def _validate_references_from_repository(self, repository):
188
229
        """Now that we have a repository which should have some of the
210
251
        # All of the contained revisions were checked
211
252
        # in _validate_revisions
212
253
        checked = {}
213
 
        for rev_info in self.revisions:
 
254
        for rev_info in self.info.revisions:
214
255
            checked[rev_info.revision_id] = True
215
256
            add_sha(rev_to_sha, rev_info.revision_id, rev_info.sha1)
216
257
                
217
 
        for (rev, rev_info) in zip(self.real_revisions, self.revisions):
 
258
        for (rev, rev_info) in zip(self.info.real_revisions, self.info.revisions):
218
259
            add_sha(inv_to_sha, rev_info.revision_id, rev_info.inventory_sha1)
219
260
 
220
261
        count = 0
264
305
        s = serializer_v5.write_inventory_to_string(inv)
265
306
        sha1 = sha_string(s)
266
307
        # Target revision is the last entry in the real_revisions list
267
 
        rev = self.get_revision(revision_id)
 
308
        rev = self.info.get_revision(revision_id)
268
309
        assert rev.revision_id == revision_id
269
310
        if sha1 != rev.inventory_sha1:
270
311
            open(',,bogus-inv', 'wb').write(s)
271
312
            warning('Inventory sha hash mismatch for revision %s. %s'
272
313
                    ' != %s' % (revision_id, sha1, rev.inventory_sha1))
273
314
 
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
 
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
291
514
 
292
515
    def _update_tree(self, bundle_tree, revision_id):
293
516
        """This fills out a BundleTree based on the information
401
624
            'modified':modified
402
625
        }
403
626
        for action_line, lines in \
404
 
            self.get_revision_info(revision_id).tree_actions:
 
627
            self.info.get_revision_info(revision_id).tree_actions:
405
628
            first = action_line.find(' ')
406
629
            if first == -1:
407
630
                raise BzrError('Bogus action line'
495
718
            if old_dir is None:
496
719
                old_path = None
497
720
            else:
498
 
                old_path = pathjoin(old_dir, basename)
 
721
                old_path = os.path.join(old_dir, basename)
499
722
        else:
500
723
            old_path = new_path
501
724
        #If the new path wasn't in renamed, the old one shouldn't be in
520
743
            if new_dir is None:
521
744
                new_path = None
522
745
            else:
523
 
                new_path = pathjoin(new_dir, basename)
 
746
                new_path = os.path.join(new_dir, basename)
524
747
        else:
525
748
            new_path = old_path
526
749
        #If the old path wasn't in renamed, the new one shouldn't be in