~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/bundle/read_bundle.py

  • Committer: Wouter van Heyst
  • Date: 2006-06-07 16:05:27 UTC
  • mto: This revision was merged to the branch mainline in revision 1752.
  • Revision ID: larstiq@larstiq.dyndns.org-20060607160527-2b3649154d0e2e84
more code cleanup

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 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
 
"""Read in a bundle stream, and process it into a BundleReader object."""
 
1
#!/usr/bin/env python
 
2
"""\
 
3
Read in a bundle stream, and process it into a BundleReader object.
 
4
"""
18
5
 
19
6
import base64
20
7
from cStringIO import StringIO
21
8
import os
22
9
import pprint
23
10
 
24
 
from bzrlib import (
25
 
    osutils,
26
 
    )
27
 
import bzrlib.errors
28
 
from bzrlib.bundle import apply_bundle
29
11
from bzrlib.errors import (TestamentMismatch, BzrError, 
30
 
                           MalformedHeader, MalformedPatches, NotABundle)
 
12
                           MalformedHeader, MalformedPatches)
 
13
from bzrlib.bundle.common import get_header, header_str
31
14
from bzrlib.inventory import (Inventory, InventoryEntry,
32
15
                              InventoryDirectory, InventoryFile,
33
16
                              InventoryLink)
34
 
from bzrlib.osutils import sha_file, sha_string, pathjoin
 
17
from bzrlib.osutils import sha_file, sha_string
35
18
from bzrlib.revision import Revision, NULL_REVISION
36
19
from bzrlib.testament import StrictTestament
37
20
from bzrlib.trace import mutter, warning
38
 
import bzrlib.transport
39
21
from bzrlib.tree import Tree
40
 
import bzrlib.urlutils
41
22
from bzrlib.xml5 import serializer_v5
42
23
 
43
24
 
76
57
        if self.properties:
77
58
            for property in self.properties:
78
59
                key_end = property.find(': ')
79
 
                if key_end == -1:
80
 
                    assert property.endswith(':')
81
 
                    key = str(property[:-1])
82
 
                    value = ''
83
 
                else:
84
 
                    key = str(property[:key_end])
85
 
                    value = property[key_end+2:]
 
60
                assert key_end is not None
 
61
                key = property[:key_end].encode('utf-8')
 
62
                value = property[key_end+2:].encode('utf-8')
86
63
                rev.properties[key] = value
87
64
 
88
65
        return rev
109
86
        self.timestamp = None
110
87
        self.timezone = None
111
88
 
112
 
        # Have we checked the repository yet?
113
 
        self._validated_revisions_against_repo = False
114
 
 
115
89
    def __str__(self):
116
90
        return pprint.pformat(self.__dict__)
117
91
 
120
94
        split up, based on the assumptions that can be made
121
95
        when information is missing.
122
96
        """
123
 
        from bzrlib.timestamp import unpack_highres_date
 
97
        from bzrlib.bundle.common import unpack_highres_date
124
98
        # Put in all of the guessable information.
125
99
        if not self.timestamp and self.date:
126
100
            self.timestamp, self.timezone = unpack_highres_date(self.date)
178
152
                return r
179
153
        raise KeyError(revision_id)
180
154
 
181
 
    def revision_tree(self, repository, revision_id, base=None):
182
 
        revision_id = osutils.safe_revision_id(revision_id)
183
 
        revision = self.get_revision(revision_id)
184
 
        base = self.get_base(revision)
185
 
        assert base != revision_id
186
 
        if not self._validated_revisions_against_repo:
187
 
            self._validate_references_from_repository(repository)
188
 
        revision_info = self.get_revision_info(revision_id)
189
 
        inventory_revision_id = revision_id
190
 
        bundle_tree = BundleTree(repository.revision_tree(base), 
191
 
                                  inventory_revision_id)
192
 
        self._update_tree(bundle_tree, revision_id)
193
 
 
194
 
        inv = bundle_tree.inventory
195
 
        self._validate_inventory(inv, revision_id)
196
 
        self._validate_revision(inv, revision_id)
197
 
 
198
 
        return bundle_tree
 
155
 
 
156
class BundleReader(object):
 
157
    """This class reads in a bundle from a file, and returns
 
158
    a Bundle object, which can then be applied against a tree.
 
159
    """
 
160
    def __init__(self, from_file):
 
161
        """Read in the bundle from the file.
 
162
 
 
163
        :param from_file: A file-like object (must have iterator support).
 
164
        """
 
165
        object.__init__(self)
 
166
        self.from_file = iter(from_file)
 
167
        self._next_line = None
 
168
        
 
169
        self.info = BundleInfo()
 
170
        # We put the actual inventory ids in the footer, so that the patch
 
171
        # is easier to read for humans.
 
172
        # Unfortunately, that means we need to read everything before we
 
173
        # can create a proper bundle.
 
174
        self._read()
 
175
        self._validate()
 
176
 
 
177
    def _read(self):
 
178
        self._read_header()
 
179
        while self._next_line is not None:
 
180
            self._read_revision_header()
 
181
            if self._next_line is None:
 
182
                break
 
183
            self._read_patches()
 
184
            self._read_footer()
 
185
 
 
186
    def _validate(self):
 
187
        """Make sure that the information read in makes sense
 
188
        and passes appropriate checksums.
 
189
        """
 
190
        # Fill in all the missing blanks for the revisions
 
191
        # and generate the real_revisions list.
 
192
        self.info.complete_info()
 
193
 
 
194
    def _validate_revision(self, inventory, revision_id):
 
195
        """Make sure all revision entries match their checksum."""
 
196
 
 
197
        # This is a mapping from each revision id to it's sha hash
 
198
        rev_to_sha1 = {}
 
199
        
 
200
        rev = self.info.get_revision(revision_id)
 
201
        rev_info = self.info.get_revision_info(revision_id)
 
202
        assert rev.revision_id == rev_info.revision_id
 
203
        assert rev.revision_id == revision_id
 
204
        sha1 = StrictTestament(rev, inventory).as_sha1()
 
205
        if sha1 != rev_info.sha1:
 
206
            raise TestamentMismatch(rev.revision_id, rev_info.sha1, sha1)
 
207
        if rev_to_sha1.has_key(rev.revision_id):
 
208
            raise BzrError('Revision {%s} given twice in the list'
 
209
                    % (rev.revision_id))
 
210
        rev_to_sha1[rev.revision_id] = sha1
199
211
 
200
212
    def _validate_references_from_repository(self, repository):
201
213
        """Now that we have a repository which should have some of the
223
235
        # All of the contained revisions were checked
224
236
        # in _validate_revisions
225
237
        checked = {}
226
 
        for rev_info in self.revisions:
 
238
        for rev_info in self.info.revisions:
227
239
            checked[rev_info.revision_id] = True
228
240
            add_sha(rev_to_sha, rev_info.revision_id, rev_info.sha1)
229
241
                
230
 
        for (rev, rev_info) in zip(self.real_revisions, self.revisions):
 
242
        for (rev, rev_info) in zip(self.info.real_revisions, self.info.revisions):
231
243
            add_sha(inv_to_sha, rev_info.revision_id, rev_info.inventory_sha1)
232
244
 
233
245
        count = 0
236
248
            if repository.has_revision(revision_id):
237
249
                testament = StrictTestament.from_revision(repository, 
238
250
                                                          revision_id)
239
 
                local_sha1 = self._testament_sha1_from_revision(repository,
240
 
                                                                revision_id)
 
251
                local_sha1 = testament.as_sha1()
241
252
                if sha1 != local_sha1:
242
253
                    raise BzrError('sha1 mismatch. For revision id {%s}' 
243
254
                            'local: %s, bundle: %s' % (revision_id, local_sha1, sha1))
266
277
            warning('Not all revision hashes could be validated.'
267
278
                    ' Unable validate %d hashes' % len(missing))
268
279
        mutter('Verified %d sha hashes for the bundle.' % count)
269
 
        self._validated_revisions_against_repo = True
270
280
 
271
281
    def _validate_inventory(self, inv, revision_id):
272
282
        """At this point we should have generated the BundleTree,
279
289
        s = serializer_v5.write_inventory_to_string(inv)
280
290
        sha1 = sha_string(s)
281
291
        # Target revision is the last entry in the real_revisions list
282
 
        rev = self.get_revision(revision_id)
 
292
        rev = self.info.get_revision(revision_id)
283
293
        assert rev.revision_id == revision_id
284
294
        if sha1 != rev.inventory_sha1:
285
295
            open(',,bogus-inv', 'wb').write(s)
286
296
            warning('Inventory sha hash mismatch for revision %s. %s'
287
297
                    ' != %s' % (revision_id, sha1, rev.inventory_sha1))
288
298
 
289
 
    def _validate_revision(self, inventory, revision_id):
290
 
        """Make sure all revision entries match their checksum."""
291
 
 
292
 
        # This is a mapping from each revision id to it's sha hash
293
 
        rev_to_sha1 = {}
294
 
        
295
 
        rev = self.get_revision(revision_id)
296
 
        rev_info = self.get_revision_info(revision_id)
297
 
        assert rev.revision_id == rev_info.revision_id
298
 
        assert rev.revision_id == revision_id
299
 
        sha1 = self._testament_sha1(rev, inventory)
300
 
        if sha1 != rev_info.sha1:
301
 
            raise TestamentMismatch(rev.revision_id, rev_info.sha1, sha1)
302
 
        if rev.revision_id in rev_to_sha1:
303
 
            raise BzrError('Revision {%s} given twice in the list'
304
 
                    % (rev.revision_id))
305
 
        rev_to_sha1[rev.revision_id] = sha1
 
299
    def get_bundle(self, repository):
 
300
        """Return the meta information, and a Bundle tree which can
 
301
        be used to populate the local stores and working tree, respectively.
 
302
        """
 
303
        return self.info, self.revision_tree(repository, self.info.target)
 
304
 
 
305
    def revision_tree(self, repository, revision_id, base=None):
 
306
        revision = self.info.get_revision(revision_id)
 
307
        base = self.info.get_base(revision)
 
308
        assert base != revision_id
 
309
        self._validate_references_from_repository(repository)
 
310
        revision_info = self.info.get_revision_info(revision_id)
 
311
        inventory_revision_id = revision_id
 
312
        bundle_tree = BundleTree(repository.revision_tree(base), 
 
313
                                  inventory_revision_id)
 
314
        self._update_tree(bundle_tree, revision_id)
 
315
 
 
316
        inv = bundle_tree.inventory
 
317
        self._validate_inventory(inv, revision_id)
 
318
        self._validate_revision(inv, revision_id)
 
319
 
 
320
        return bundle_tree
 
321
 
 
322
    def _next(self):
 
323
        """yield the next line, but secretly
 
324
        keep 1 extra line for peeking.
 
325
        """
 
326
        for line in self.from_file:
 
327
            last = self._next_line
 
328
            self._next_line = line
 
329
            if last is not None:
 
330
                #mutter('yielding line: %r' % last)
 
331
                yield last
 
332
        last = self._next_line
 
333
        self._next_line = None
 
334
        #mutter('yielding line: %r' % last)
 
335
        yield last
 
336
 
 
337
    def _read_header(self):
 
338
        """Read the bzr header"""
 
339
        header = get_header()
 
340
        found = False
 
341
        for line in self._next():
 
342
            if found:
 
343
                # not all mailers will keep trailing whitespace
 
344
                if line == '#\n':
 
345
                    line = '# \n'
 
346
                if (not line.startswith('# ') or not line.endswith('\n')
 
347
                        or line[2:-1].decode('utf-8') != header[0]):
 
348
                    raise MalformedHeader('Found a header, but it'
 
349
                        ' was improperly formatted')
 
350
                header.pop(0) # We read this line.
 
351
                if not header:
 
352
                    break # We found everything.
 
353
            elif (line.startswith('#') and line.endswith('\n')):
 
354
                line = line[1:-1].strip().decode('utf-8')
 
355
                if line[:len(header_str)] == header_str:
 
356
                    if line == header[0]:
 
357
                        found = True
 
358
                    else:
 
359
                        raise MalformedHeader('Found what looks like'
 
360
                                ' a header, but did not match')
 
361
                    header.pop(0)
 
362
        else:
 
363
            raise MalformedHeader('Did not find an opening header')
 
364
 
 
365
    def _read_revision_header(self):
 
366
        self.info.revisions.append(RevisionInfo(None))
 
367
        for line in self._next():
 
368
            # The bzr header is terminated with a blank line
 
369
            # which does not start with '#'
 
370
            if line is None or line == '\n':
 
371
                break
 
372
            self._handle_next(line)
 
373
 
 
374
    def _read_next_entry(self, line, indent=1):
 
375
        """Read in a key-value pair
 
376
        """
 
377
        if not line.startswith('#'):
 
378
            raise MalformedHeader('Bzr header did not start with #')
 
379
        line = line[1:-1].decode('utf-8') # Remove the '#' and '\n'
 
380
        if line[:indent] == ' '*indent:
 
381
            line = line[indent:]
 
382
        if not line:
 
383
            return None, None# Ignore blank lines
 
384
 
 
385
        loc = line.find(': ')
 
386
        if loc != -1:
 
387
            key = line[:loc]
 
388
            value = line[loc+2:]
 
389
            if not value:
 
390
                value = self._read_many(indent=indent+2)
 
391
        elif line[-1:] == ':':
 
392
            key = line[:-1]
 
393
            value = self._read_many(indent=indent+2)
 
394
        else:
 
395
            raise MalformedHeader('While looking for key: value pairs,'
 
396
                    ' did not find the colon %r' % (line))
 
397
 
 
398
        key = key.replace(' ', '_')
 
399
        #mutter('found %s: %s' % (key, value))
 
400
        return key, value
 
401
 
 
402
    def _handle_next(self, line):
 
403
        if line is None:
 
404
            return
 
405
        key, value = self._read_next_entry(line, indent=1)
 
406
        mutter('_handle_next %r => %r' % (key, value))
 
407
        if key is None:
 
408
            return
 
409
 
 
410
        revision_info = self.info.revisions[-1]
 
411
        if hasattr(revision_info, key):
 
412
            if getattr(revision_info, key) is None:
 
413
                setattr(revision_info, key, value)
 
414
            else:
 
415
                raise MalformedHeader('Duplicated Key: %s' % key)
 
416
        else:
 
417
            # What do we do with a key we don't recognize
 
418
            raise MalformedHeader('Unknown Key: "%s"' % key)
 
419
    
 
420
    def _read_many(self, indent):
 
421
        """If a line ends with no entry, that means that it should be
 
422
        followed with multiple lines of values.
 
423
 
 
424
        This detects the end of the list, because it will be a line that
 
425
        does not start properly indented.
 
426
        """
 
427
        values = []
 
428
        start = '#' + (' '*indent)
 
429
 
 
430
        if self._next_line is None or self._next_line[:len(start)] != start:
 
431
            return values
 
432
 
 
433
        for line in self._next():
 
434
            values.append(line[len(start):-1].decode('utf-8'))
 
435
            if self._next_line is None or self._next_line[:len(start)] != start:
 
436
                break
 
437
        return values
 
438
 
 
439
    def _read_one_patch(self):
 
440
        """Read in one patch, return the complete patch, along with
 
441
        the next line.
 
442
 
 
443
        :return: action, lines, do_continue
 
444
        """
 
445
        #mutter('_read_one_patch: %r' % self._next_line)
 
446
        # Peek and see if there are no patches
 
447
        if self._next_line is None or self._next_line.startswith('#'):
 
448
            return None, [], False
 
449
 
 
450
        first = True
 
451
        lines = []
 
452
        for line in self._next():
 
453
            if first:
 
454
                if not line.startswith('==='):
 
455
                    raise MalformedPatches('The first line of all patches'
 
456
                        ' should be a bzr meta line "==="'
 
457
                        ': %r' % line)
 
458
                action = line[4:-1].decode('utf-8')
 
459
            elif line.startswith('... '):
 
460
                action += line[len('... '):-1].decode('utf-8')
 
461
 
 
462
            if (self._next_line is not None and 
 
463
                self._next_line.startswith('===')):
 
464
                return action, lines, True
 
465
            elif self._next_line is None or self._next_line.startswith('#'):
 
466
                return action, lines, False
 
467
 
 
468
            if first:
 
469
                first = False
 
470
            elif not line.startswith('... '):
 
471
                lines.append(line)
 
472
 
 
473
        return action, lines, False
 
474
            
 
475
    def _read_patches(self):
 
476
        do_continue = True
 
477
        revision_actions = []
 
478
        while do_continue:
 
479
            action, lines, do_continue = self._read_one_patch()
 
480
            if action is not None:
 
481
                revision_actions.append((action, lines))
 
482
        assert self.info.revisions[-1].tree_actions is None
 
483
        self.info.revisions[-1].tree_actions = revision_actions
 
484
 
 
485
    def _read_footer(self):
 
486
        """Read the rest of the meta information.
 
487
 
 
488
        :param first_line:  The previous step iterates past what it
 
489
                            can handle. That extra line is given here.
 
490
        """
 
491
        for line in self._next():
 
492
            self._handle_next(line)
 
493
            if not self._next_line.startswith('#'):
 
494
                self._next().next()
 
495
                break
 
496
            if self._next_line is None:
 
497
                break
306
498
 
307
499
    def _update_tree(self, bundle_tree, revision_id):
308
500
        """This fills out a BundleTree based on the information
313
505
 
314
506
        def get_rev_id(last_changed, path, kind):
315
507
            if last_changed is not None:
316
 
                # last_changed will be a Unicode string because of how it was
317
 
                # read. Convert it back to utf8.
318
 
                changed_revision_id = osutils.safe_revision_id(last_changed,
319
 
                                                               warn=False)
 
508
                changed_revision_id = last_changed.decode('utf-8')
320
509
            else:
321
510
                changed_revision_id = revision_id
322
511
            bundle_tree.note_last_changed(path, changed_revision_id)
389
578
            if not info[1].startswith('file-id:'):
390
579
                raise BzrError('The file-id should follow the path for an add'
391
580
                        ': %r' % extra)
392
 
            # This will be Unicode because of how the stream is read. Turn it
393
 
            # back into a utf8 file_id
394
 
            file_id = osutils.safe_file_id(info[1][8:], warn=False)
 
581
            file_id = info[1][8:]
395
582
 
396
583
            bundle_tree.note_id(file_id, path, kind)
397
584
            # this will be overridden in extra_info if executable is specified.
421
608
            'modified':modified
422
609
        }
423
610
        for action_line, lines in \
424
 
            self.get_revision_info(revision_id).tree_actions:
 
611
            self.info.get_revision_info(revision_id).tree_actions:
425
612
            first = action_line.find(' ')
426
613
            if first == -1:
427
614
                raise BzrError('Bogus action line'
442
629
                        ' (unrecognized action): %r' % action_line)
443
630
            valid_actions[action](kind, extra, lines)
444
631
 
445
 
    def install_revisions(self, target_repo):
446
 
        """Install revisions and return the target revision"""
447
 
        apply_bundle.install_bundle(target_repo, self)
448
 
        return self.target
449
 
 
450
632
 
451
633
class BundleTree(Tree):
452
634
    def __init__(self, base_tree, revision_id):
470
652
 
471
653
    def note_rename(self, old_path, new_path):
472
654
        """A file/directory has been renamed from old_path => new_path"""
473
 
        assert new_path not in self._renamed
474
 
        assert old_path not in self._renamed_r
 
655
        assert not self._renamed.has_key(new_path)
 
656
        assert not self._renamed_r.has_key(old_path)
475
657
        self._renamed[new_path] = old_path
476
658
        self._renamed_r[old_path] = new_path
477
659
 
482
664
        self._kinds[new_id] = kind
483
665
 
484
666
    def note_last_changed(self, file_id, revision_id):
485
 
        if (file_id in self._last_changed
 
667
        if (self._last_changed.has_key(file_id)
486
668
                and self._last_changed[file_id] != revision_id):
487
669
            raise BzrError('Mismatched last-changed revision for file_id {%s}'
488
670
                    ': %s != %s' % (file_id,
520
702
            if old_dir is None:
521
703
                old_path = None
522
704
            else:
523
 
                old_path = pathjoin(old_dir, basename)
 
705
                old_path = os.path.join(old_dir, basename)
524
706
        else:
525
707
            old_path = new_path
526
708
        #If the new path wasn't in renamed, the old one shouldn't be in
527
709
        #renamed_r
528
 
        if old_path in self._renamed_r:
 
710
        if self._renamed_r.has_key(old_path):
529
711
            return None
530
712
        return old_path 
531
713
 
537
719
        new_path = self._renamed_r.get(old_path)
538
720
        if new_path is not None:
539
721
            return new_path
540
 
        if new_path in self._renamed:
 
722
        if self._renamed.has_key(new_path):
541
723
            return None
542
724
        dirname,basename = os.path.split(old_path)
543
725
        if dirname != '':
545
727
            if new_dir is None:
546
728
                new_path = None
547
729
            else:
548
 
                new_path = pathjoin(new_dir, basename)
 
730
                new_path = os.path.join(new_dir, basename)
549
731
        else:
550
732
            new_path = old_path
551
733
        #If the old path wasn't in renamed, the new one shouldn't be in
552
734
        #renamed_r
553
 
        if new_path in self._renamed:
 
735
        if self._renamed.has_key(new_path):
554
736
            return None
555
737
        return new_path 
556
738
 
564
746
            return None
565
747
        if old_path in self.deleted:
566
748
            return None
567
 
        if getattr(self.base_tree, 'path2id', None) is not None:
 
749
        if hasattr(self.base_tree, 'path2id'):
568
750
            return self.base_tree.path2id(old_path)
569
751
        else:
570
752
            return self.base_tree.inventory.path2id(old_path)
602
784
                then be cached.
603
785
        """
604
786
        base_id = self.old_contents_id(file_id)
605
 
        if (base_id is not None and
606
 
            base_id != self.base_tree.inventory.root.file_id):
 
787
        if base_id is not None:
607
788
            patch_original = self.base_tree.get_file(base_id)
608
789
        else:
609
790
            patch_original = None
672
853
 
673
854
        assert self.base_tree is not None
674
855
        base_inv = self.base_tree.inventory
675
 
        inv = Inventory(None, self.revision_id)
 
856
        root_id = base_inv.root.file_id
 
857
        try:
 
858
            # New inventories have a unique root_id
 
859
            inv = Inventory(root_id, self.revision_id)
 
860
        except TypeError:
 
861
            inv = Inventory(revision_id=self.revision_id)
676
862
 
677
863
        def add_entry(file_id):
678
864
            path = self.id2path(file_id)
679
865
            if path is None:
680
866
                return
681
 
            if path == '':
682
 
                parent_id = None
 
867
            parent_path = dirname(path)
 
868
            if parent_path == u'':
 
869
                parent_id = root_id
683
870
            else:
684
 
                parent_path = dirname(path)
685
871
                parent_id = self.path2id(parent_path)
686
872
 
687
873
            kind = self.get_kind(file_id)
708
894
 
709
895
        sorted_entries = self.sorted_path_id()
710
896
        for path, file_id in sorted_entries:
 
897
            if file_id == inv.root.file_id:
 
898
                continue
711
899
            add_entry(file_id)
712
900
 
713
901
        return inv
742
930
    from bzrlib.iterablefile import IterableFile
743
931
    if file_patch == "":
744
932
        return IterableFile(())
745
 
    # string.splitlines(True) also splits on '\r', but the iter_patched code
746
 
    # only expects to iterate over '\n' style lines
747
 
    return IterableFile(iter_patched(original,
748
 
                StringIO(file_patch).readlines()))
 
933
    return IterableFile(iter_patched(original, file_patch.splitlines(True)))