~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/bundle/bundle_data.py

  • Committer: John Arbash Meinel
  • Date: 2007-05-03 16:42:30 UTC
  • mto: This revision was merged to the branch mainline in revision 2481.
  • Revision ID: john@arbash-meinel.com-20070503164230-y0411liq6w3bphj0
Vastly improve bundle install performance by only validating the bundle one time

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005-2010 Canonical Ltd
 
1
# Copyright (C) 2006 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
12
12
#
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
 
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
17
17
"""Read in a bundle stream, and process it into a BundleReader object."""
18
18
 
19
 
from __future__ import absolute_import
20
 
 
21
19
import base64
22
20
from cStringIO import StringIO
23
21
import os
25
23
 
26
24
from bzrlib import (
27
25
    osutils,
28
 
    timestamp,
29
26
    )
 
27
import bzrlib.errors
30
28
from bzrlib.bundle import apply_bundle
31
 
from bzrlib.errors import (
32
 
    TestamentMismatch,
33
 
    BzrError,
34
 
    )
35
 
from bzrlib.inventory import (
36
 
    Inventory,
37
 
    InventoryDirectory,
38
 
    InventoryFile,
39
 
    InventoryLink,
40
 
    )
41
 
from bzrlib.osutils import sha_string, pathjoin
 
29
from bzrlib.errors import (TestamentMismatch, BzrError, 
 
30
                           MalformedHeader, MalformedPatches, NotABundle)
 
31
from bzrlib.inventory import (Inventory, InventoryEntry,
 
32
                              InventoryDirectory, InventoryFile,
 
33
                              InventoryLink)
 
34
from bzrlib.osutils import sha_file, sha_string, pathjoin
42
35
from bzrlib.revision import Revision, NULL_REVISION
43
36
from bzrlib.testament import StrictTestament
44
37
from bzrlib.trace import mutter, warning
 
38
import bzrlib.transport
45
39
from bzrlib.tree import Tree
 
40
import bzrlib.urlutils
46
41
from bzrlib.xml5 import serializer_v5
47
42
 
48
43
 
82
77
            for property in self.properties:
83
78
                key_end = property.find(': ')
84
79
                if key_end == -1:
85
 
                    if not property.endswith(':'):
86
 
                        raise ValueError(property)
 
80
                    assert property.endswith(':')
87
81
                    key = str(property[:-1])
88
82
                    value = ''
89
83
                else:
93
87
 
94
88
        return rev
95
89
 
96
 
    @staticmethod
97
 
    def from_revision(revision):
98
 
        revision_info = RevisionInfo(revision.revision_id)
99
 
        date = timestamp.format_highres_date(revision.timestamp,
100
 
                                             revision.timezone)
101
 
        revision_info.date = date
102
 
        revision_info.timezone = revision.timezone
103
 
        revision_info.timestamp = revision.timestamp
104
 
        revision_info.message = revision.message.split('\n')
105
 
        revision_info.properties = [': '.join(p) for p in
106
 
                                    revision.properties.iteritems()]
107
 
        return revision_info
108
 
 
109
90
 
110
91
class BundleInfo(object):
111
92
    """This contains the meta information. Stuff that allows you to
112
93
    recreate the revision or inventory XML.
113
94
    """
114
 
    def __init__(self, bundle_format=None):
115
 
        self.bundle_format = None
 
95
    def __init__(self):
116
96
        self.committer = None
117
97
        self.date = None
118
98
        self.message = None
163
143
    def get_base(self, revision):
164
144
        revision_info = self.get_revision_info(revision.revision_id)
165
145
        if revision_info.base_id is not None:
166
 
            return revision_info.base_id
 
146
            if revision_info.base_id == NULL_REVISION:
 
147
                return None
 
148
            else:
 
149
                return revision_info.base_id
167
150
        if len(revision.parent_ids) == 0:
168
151
            # There is no base listed, and
169
152
            # the lowest revision doesn't have a parent
170
153
            # so this is probably against the empty tree
171
 
            # and thus base truly is NULL_REVISION
172
 
            return NULL_REVISION
 
154
            # and thus base truly is None
 
155
            return None
173
156
        else:
174
157
            return revision.parent_ids[-1]
175
158
 
196
179
        raise KeyError(revision_id)
197
180
 
198
181
    def revision_tree(self, repository, revision_id, base=None):
 
182
        revision_id = osutils.safe_revision_id(revision_id)
199
183
        revision = self.get_revision(revision_id)
200
184
        base = self.get_base(revision)
201
 
        if base == revision_id:
202
 
            raise AssertionError()
 
185
        assert base != revision_id
203
186
        if not self._validated_revisions_against_repo:
204
187
            self._validate_references_from_repository(repository)
205
188
        revision_info = self.get_revision_info(revision_id)
206
189
        inventory_revision_id = revision_id
207
 
        bundle_tree = BundleTree(repository.revision_tree(base),
 
190
        bundle_tree = BundleTree(repository.revision_tree(base), 
208
191
                                  inventory_revision_id)
209
192
        self._update_tree(bundle_tree, revision_id)
210
193
 
211
194
        inv = bundle_tree.inventory
212
195
        self._validate_inventory(inv, revision_id)
213
 
        self._validate_revision(bundle_tree, revision_id)
 
196
        self._validate_revision(inv, revision_id)
214
197
 
215
198
        return bundle_tree
216
199
 
243
226
        for rev_info in self.revisions:
244
227
            checked[rev_info.revision_id] = True
245
228
            add_sha(rev_to_sha, rev_info.revision_id, rev_info.sha1)
246
 
 
 
229
                
247
230
        for (rev, rev_info) in zip(self.real_revisions, self.revisions):
248
231
            add_sha(inv_to_sha, rev_info.revision_id, rev_info.inventory_sha1)
249
232
 
251
234
        missing = {}
252
235
        for revision_id, sha1 in rev_to_sha.iteritems():
253
236
            if repository.has_revision(revision_id):
254
 
                testament = StrictTestament.from_revision(repository,
 
237
                testament = StrictTestament.from_revision(repository, 
255
238
                                                          revision_id)
256
239
                local_sha1 = self._testament_sha1_from_revision(repository,
257
240
                                                                revision_id)
258
241
                if sha1 != local_sha1:
259
 
                    raise BzrError('sha1 mismatch. For revision id {%s}'
 
242
                    raise BzrError('sha1 mismatch. For revision id {%s}' 
260
243
                            'local: %s, bundle: %s' % (revision_id, local_sha1, sha1))
261
244
                else:
262
245
                    count += 1
263
246
            elif revision_id not in checked:
264
247
                missing[revision_id] = sha1
265
248
 
 
249
        for inv_id, sha1 in inv_to_sha.iteritems():
 
250
            if repository.has_revision(inv_id):
 
251
                # Note: branch.get_inventory_sha1() just returns the value that
 
252
                # is stored in the revision text, and that value may be out
 
253
                # of date. This is bogus, because that means we aren't
 
254
                # validating the actual text, just that we wrote and read the
 
255
                # string. But for now, what the hell.
 
256
                local_sha1 = repository.get_inventory_sha1(inv_id)
 
257
                if sha1 != local_sha1:
 
258
                    raise BzrError('sha1 mismatch. For inventory id {%s}' 
 
259
                                   'local: %s, bundle: %s' % 
 
260
                                   (inv_id, local_sha1, sha1))
 
261
                else:
 
262
                    count += 1
 
263
 
266
264
        if len(missing) > 0:
267
265
            # I don't know if this is an error yet
268
266
            warning('Not all revision hashes could be validated.'
274
272
        """At this point we should have generated the BundleTree,
275
273
        so build up an inventory, and make sure the hashes match.
276
274
        """
 
275
 
 
276
        assert inv is not None
 
277
 
277
278
        # Now we should have a complete inventory entry.
278
279
        s = serializer_v5.write_inventory_to_string(inv)
279
280
        sha1 = sha_string(s)
280
281
        # Target revision is the last entry in the real_revisions list
281
282
        rev = self.get_revision(revision_id)
282
 
        if rev.revision_id != revision_id:
283
 
            raise AssertionError()
 
283
        assert rev.revision_id == revision_id
284
284
        if sha1 != rev.inventory_sha1:
285
 
            f = open(',,bogus-inv', 'wb')
286
 
            try:
287
 
                f.write(s)
288
 
            finally:
289
 
                f.close()
 
285
            open(',,bogus-inv', 'wb').write(s)
290
286
            warning('Inventory sha hash mismatch for revision %s. %s'
291
287
                    ' != %s' % (revision_id, sha1, rev.inventory_sha1))
292
288
 
293
 
    def _validate_revision(self, tree, revision_id):
 
289
    def _validate_revision(self, inventory, revision_id):
294
290
        """Make sure all revision entries match their checksum."""
295
291
 
296
 
        # This is a mapping from each revision id to its sha hash
 
292
        # This is a mapping from each revision id to it's sha hash
297
293
        rev_to_sha1 = {}
298
 
 
 
294
        
299
295
        rev = self.get_revision(revision_id)
300
296
        rev_info = self.get_revision_info(revision_id)
301
 
        if not (rev.revision_id == rev_info.revision_id):
302
 
            raise AssertionError()
303
 
        if not (rev.revision_id == revision_id):
304
 
            raise AssertionError()
305
 
        sha1 = self._testament_sha1(rev, tree)
 
297
        assert rev.revision_id == rev_info.revision_id
 
298
        assert rev.revision_id == revision_id
 
299
        sha1 = self._testament_sha1(rev, inventory)
306
300
        if sha1 != rev_info.sha1:
307
301
            raise TestamentMismatch(rev.revision_id, rev_info.sha1, sha1)
308
302
        if rev.revision_id in rev_to_sha1:
335
329
                try:
336
330
                    name, value = info_item.split(':', 1)
337
331
                except ValueError:
338
 
                    raise ValueError('Value %r has no colon' % info_item)
 
332
                    raise 'Value %r has no colon' % info_item
339
333
                if name == 'last-changed':
340
334
                    last_changed = value
341
335
                elif name == 'executable':
 
336
                    assert value in ('yes', 'no'), value
342
337
                    val = (value == 'yes')
343
338
                    bundle_tree.note_executable(new_path, val)
344
339
                elif name == 'target':
348
343
            return last_changed, encoding
349
344
 
350
345
        def do_patch(path, lines, encoding):
351
 
            if encoding == 'base64':
 
346
            if encoding is not None:
 
347
                assert encoding == 'base64'
352
348
                patch = base64.decodestring(''.join(lines))
353
 
            elif encoding is None:
 
349
            else:
354
350
                patch =  ''.join(lines)
355
 
            else:
356
 
                raise ValueError(encoding)
357
351
            bundle_tree.note_patch(path, patch)
358
352
 
359
353
        def renamed(kind, extra, lines):
419
413
            revision = get_rev_id(last_modified, path, kind)
420
414
            if lines:
421
415
                do_patch(path, lines, encoding)
422
 
 
 
416
            
423
417
        valid_actions = {
424
418
            'renamed':renamed,
425
419
            'removed':removed,
448
442
                        ' (unrecognized action): %r' % action_line)
449
443
            valid_actions[action](kind, extra, lines)
450
444
 
451
 
    def install_revisions(self, target_repo, stream_input=True):
452
 
        """Install revisions and return the target revision
453
 
 
454
 
        :param target_repo: The repository to install into
455
 
        :param stream_input: Ignored by this implementation.
456
 
        """
 
445
    def install_revisions(self, target_repo):
 
446
        """Install revisions and return the target revision"""
457
447
        apply_bundle.install_bundle(target_repo, self)
458
448
        return self.target
459
449
 
460
 
    def get_merge_request(self, target_repo):
461
 
        """Provide data for performing a merge
462
 
 
463
 
        Returns suggested base, suggested target, and patch verification status
464
 
        """
465
 
        return None, self.target, 'inapplicable'
466
 
 
467
450
 
468
451
class BundleTree(Tree):
469
 
 
470
452
    def __init__(self, base_tree, revision_id):
471
453
        self.base_tree = base_tree
472
454
        self._renamed = {} # Mapping from old_path => new_path
488
470
 
489
471
    def note_rename(self, old_path, new_path):
490
472
        """A file/directory has been renamed from old_path => new_path"""
491
 
        if new_path in self._renamed:
492
 
            raise AssertionError(new_path)
493
 
        if old_path in self._renamed_r:
494
 
            raise AssertionError(old_path)
 
473
        assert new_path not in self._renamed
 
474
        assert old_path not in self._renamed_r
495
475
        self._renamed[new_path] = old_path
496
476
        self._renamed_r[old_path] = new_path
497
477
 
527
507
 
528
508
    def old_path(self, new_path):
529
509
        """Get the old_path (path in the base_tree) for the file at new_path"""
530
 
        if new_path[:1] in ('\\', '/'):
531
 
            raise ValueError(new_path)
 
510
        assert new_path[:1] not in ('\\', '/')
532
511
        old_path = self._renamed.get(new_path)
533
512
        if old_path is not None:
534
513
            return old_path
548
527
        #renamed_r
549
528
        if old_path in self._renamed_r:
550
529
            return None
551
 
        return old_path
 
530
        return old_path 
552
531
 
553
532
    def new_path(self, old_path):
554
533
        """Get the new_path (path in the target_tree) for the file at old_path
555
534
        in the base tree.
556
535
        """
557
 
        if old_path[:1] in ('\\', '/'):
558
 
            raise ValueError(old_path)
 
536
        assert old_path[:1] not in ('\\', '/')
559
537
        new_path = self._renamed_r.get(old_path)
560
538
        if new_path is not None:
561
539
            return new_path
574
552
        #renamed_r
575
553
        if new_path in self._renamed:
576
554
            return None
577
 
        return new_path
578
 
 
579
 
    def get_root_id(self):
580
 
        return self.path2id('')
 
555
        return new_path 
581
556
 
582
557
    def path2id(self, path):
583
558
        """Return the id of the file present at path in the target tree."""
589
564
            return None
590
565
        if old_path in self.deleted:
591
566
            return None
592
 
        return self.base_tree.path2id(old_path)
 
567
        if getattr(self.base_tree, 'path2id', None) is not None:
 
568
            return self.base_tree.path2id(old_path)
 
569
        else:
 
570
            return self.base_tree.inventory.path2id(old_path)
593
571
 
594
572
    def id2path(self, file_id):
595
573
        """Return the new path in the target tree of the file with id file_id"""
614
592
                return None
615
593
        new_path = self.id2path(file_id)
616
594
        return self.base_tree.path2id(new_path)
617
 
 
 
595
        
618
596
    def get_file(self, file_id):
619
597
        """Return a file-like object containing the new contents of the
620
598
        file given by file_id.
625
603
        """
626
604
        base_id = self.old_contents_id(file_id)
627
605
        if (base_id is not None and
628
 
            base_id != self.base_tree.get_root_id()):
 
606
            base_id != self.base_tree.inventory.root.file_id):
629
607
            patch_original = self.base_tree.get_file(base_id)
630
608
        else:
631
609
            patch_original = None
632
610
        file_patch = self.patches.get(self.id2path(file_id))
633
611
        if file_patch is None:
634
 
            if (patch_original is None and
635
 
                self.kind(file_id) == 'directory'):
 
612
            if (patch_original is None and 
 
613
                self.get_kind(file_id) == 'directory'):
636
614
                return StringIO()
637
 
            if patch_original is None:
638
 
                raise AssertionError("None: %s" % file_id)
 
615
            assert patch_original is not None, "None: %s" % file_id
639
616
            return patch_original
640
617
 
641
 
        if file_patch.startswith('\\'):
642
 
            raise ValueError(
643
 
                'Malformed patch for %s, %r' % (file_id, file_patch))
 
618
        assert not file_patch.startswith('\\'), \
 
619
            'Malformed patch for %s, %r' % (file_id, file_patch)
644
620
        return patched_file(file_patch, patch_original)
645
621
 
646
 
    def get_symlink_target(self, file_id, path=None):
647
 
        if path is None:
648
 
            path = self.id2path(file_id)
 
622
    def get_symlink_target(self, file_id):
 
623
        new_path = self.id2path(file_id)
649
624
        try:
650
 
            return self._targets[path]
 
625
            return self._targets[new_path]
651
626
        except KeyError:
652
627
            return self.base_tree.get_symlink_target(file_id)
653
628
 
654
 
    def kind(self, file_id):
 
629
    def get_kind(self, file_id):
655
630
        if file_id in self._kinds:
656
631
            return self._kinds[file_id]
657
 
        return self.base_tree.kind(file_id)
658
 
 
659
 
    def get_file_revision(self, file_id):
660
 
        path = self.id2path(file_id)
661
 
        if path in self._last_changed:
662
 
            return self._last_changed[path]
663
 
        else:
664
 
            return self.base_tree.get_file_revision(file_id)
 
632
        return self.base_tree.inventory[file_id].kind
665
633
 
666
634
    def is_executable(self, file_id):
667
635
        path = self.id2path(file_id)
668
636
        if path in self._executable:
669
637
            return self._executable[path]
670
638
        else:
671
 
            return self.base_tree.is_executable(file_id)
 
639
            return self.base_tree.inventory[file_id].executable
672
640
 
673
641
    def get_last_changed(self, file_id):
674
642
        path = self.id2path(file_id)
675
643
        if path in self._last_changed:
676
644
            return self._last_changed[path]
677
 
        return self.base_tree.get_file_revision(file_id)
 
645
        return self.base_tree.inventory[file_id].revision
678
646
 
679
647
    def get_size_and_sha1(self, file_id):
680
648
        """Return the size and sha1 hash of the given file id.
687
655
        if new_path not in self.patches:
688
656
            # If the entry does not have a patch, then the
689
657
            # contents must be the same as in the base_tree
690
 
            text_size = self.base_tree.get_file_size(file_id)
691
 
            text_sha1 = self.base_tree.get_file_sha1(file_id)
692
 
            return text_size, text_sha1
 
658
            ie = self.base_tree.inventory[file_id]
 
659
            if ie.text_size is None:
 
660
                return ie.text_size, ie.text_sha1
 
661
            return int(ie.text_size), ie.text_sha1
693
662
        fileobj = self.get_file(file_id)
694
663
        content = fileobj.read()
695
664
        return len(content), sha_string(content)
700
669
        This need to be called before ever accessing self.inventory
701
670
        """
702
671
        from os.path import dirname, basename
 
672
 
 
673
        assert self.base_tree is not None
 
674
        base_inv = self.base_tree.inventory
703
675
        inv = Inventory(None, self.revision_id)
704
676
 
705
677
        def add_entry(file_id):
712
684
                parent_path = dirname(path)
713
685
                parent_id = self.path2id(parent_path)
714
686
 
715
 
            kind = self.kind(file_id)
 
687
            kind = self.get_kind(file_id)
716
688
            revision_id = self.get_last_changed(file_id)
717
689
 
718
690
            name = basename(path)
723
695
                ie.executable = self.is_executable(file_id)
724
696
            elif kind == 'symlink':
725
697
                ie = InventoryLink(file_id, name, parent_id)
726
 
                ie.symlink_target = self.get_symlink_target(file_id, path)
 
698
                ie.symlink_target = self.get_symlink_target(file_id)
727
699
            ie.revision = revision_id
728
700
 
729
 
            if kind == 'file':
 
701
            if kind in ('directory', 'symlink'):
 
702
                ie.text_size, ie.text_sha1 = None, None
 
703
            else:
730
704
                ie.text_size, ie.text_sha1 = self.get_size_and_sha1(file_id)
731
 
                if ie.text_size is None:
732
 
                    raise BzrError(
733
 
                        'Got a text_size of None for file_id %r' % file_id)
 
705
            if (ie.text_size is None) and (kind == 'file'):
 
706
                raise BzrError('Got a text_size of None for file_id %r' % file_id)
734
707
            inv.add(ie)
735
708
 
736
709
        sorted_entries = self.sorted_path_id()
746
719
    # at that instant
747
720
    inventory = property(_get_inventory)
748
721
 
749
 
    root_inventory = property(_get_inventory)
750
 
 
751
 
    def all_file_ids(self):
752
 
        return set(
753
 
            [entry.file_id for path, entry in self.inventory.iter_entries()])
754
 
 
755
 
    def list_files(self, include_root=False, from_dir=None, recursive=True):
756
 
        # The only files returned by this are those from the version
757
 
        inv = self.inventory
758
 
        if from_dir is None:
759
 
            from_dir_id = None
760
 
        else:
761
 
            from_dir_id = inv.path2id(from_dir)
762
 
            if from_dir_id is None:
763
 
                # Directory not versioned
764
 
                return
765
 
        entries = inv.iter_entries(from_dir=from_dir_id, recursive=recursive)
766
 
        if inv.root is not None and not include_root and from_dir is None:
767
 
            # skip the root for compatability with the current apis.
768
 
            entries.next()
769
 
        for path, entry in entries:
770
 
            yield path, 'V', entry.kind, entry.file_id, entry
 
722
    def __iter__(self):
 
723
        for path, entry in self.inventory.iter_entries():
 
724
            yield entry.file_id
771
725
 
772
726
    def sorted_path_id(self):
773
727
        paths = []
774
728
        for result in self._new_id.iteritems():
775
729
            paths.append(result)
776
 
        for id in self.base_tree.all_file_ids():
 
730
        for id in self.base_tree:
777
731
            path = self.id2path(id)
778
732
            if path is None:
779
733
                continue