~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/bundle/bundle_data.py

  • Committer: Jelmer Vernooij
  • Date: 2012-02-01 19:18:09 UTC
  • mfrom: (6459 +trunk)
  • mto: This revision was merged to the branch mainline in revision 6460.
  • Revision ID: jelmer@samba.org-20120201191809-xn340a5i5v4fqsfu
Merge bzr.dev.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2006 Canonical Ltd
 
1
# Copyright (C) 2005-2010 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 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
 
19
21
import base64
20
22
from cStringIO import StringIO
21
23
import os
23
25
 
24
26
from bzrlib import (
25
27
    osutils,
 
28
    timestamp,
26
29
    )
27
 
import bzrlib.errors
28
30
from bzrlib.bundle import apply_bundle
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
 
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
35
42
from bzrlib.revision import Revision, NULL_REVISION
36
43
from bzrlib.testament import StrictTestament
37
44
from bzrlib.trace import mutter, warning
38
 
import bzrlib.transport
39
45
from bzrlib.tree import Tree
40
 
import bzrlib.urlutils
41
46
from bzrlib.xml5 import serializer_v5
42
47
 
43
48
 
77
82
            for property in self.properties:
78
83
                key_end = property.find(': ')
79
84
                if key_end == -1:
80
 
                    assert property.endswith(':')
 
85
                    if not property.endswith(':'):
 
86
                        raise ValueError(property)
81
87
                    key = str(property[:-1])
82
88
                    value = ''
83
89
                else:
87
93
 
88
94
        return rev
89
95
 
 
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
 
90
109
 
91
110
class BundleInfo(object):
92
111
    """This contains the meta information. Stuff that allows you to
93
112
    recreate the revision or inventory XML.
94
113
    """
95
 
    def __init__(self):
 
114
    def __init__(self, bundle_format=None):
 
115
        self.bundle_format = None
96
116
        self.committer = None
97
117
        self.date = None
98
118
        self.message = None
143
163
    def get_base(self, revision):
144
164
        revision_info = self.get_revision_info(revision.revision_id)
145
165
        if revision_info.base_id is not None:
146
 
            if revision_info.base_id == NULL_REVISION:
147
 
                return None
148
 
            else:
149
 
                return revision_info.base_id
 
166
            return revision_info.base_id
150
167
        if len(revision.parent_ids) == 0:
151
168
            # There is no base listed, and
152
169
            # the lowest revision doesn't have a parent
153
170
            # so this is probably against the empty tree
154
 
            # and thus base truly is None
155
 
            return None
 
171
            # and thus base truly is NULL_REVISION
 
172
            return NULL_REVISION
156
173
        else:
157
174
            return revision.parent_ids[-1]
158
175
 
179
196
        raise KeyError(revision_id)
180
197
 
181
198
    def revision_tree(self, repository, revision_id, base=None):
182
 
        revision_id = osutils.safe_revision_id(revision_id)
183
199
        revision = self.get_revision(revision_id)
184
200
        base = self.get_base(revision)
185
 
        assert base != revision_id
 
201
        if base == revision_id:
 
202
            raise AssertionError()
186
203
        if not self._validated_revisions_against_repo:
187
204
            self._validate_references_from_repository(repository)
188
205
        revision_info = self.get_revision_info(revision_id)
189
206
        inventory_revision_id = revision_id
190
 
        bundle_tree = BundleTree(repository.revision_tree(base), 
 
207
        bundle_tree = BundleTree(repository.revision_tree(base),
191
208
                                  inventory_revision_id)
192
209
        self._update_tree(bundle_tree, revision_id)
193
210
 
194
211
        inv = bundle_tree.inventory
195
212
        self._validate_inventory(inv, revision_id)
196
 
        self._validate_revision(inv, revision_id)
 
213
        self._validate_revision(bundle_tree, revision_id)
197
214
 
198
215
        return bundle_tree
199
216
 
226
243
        for rev_info in self.revisions:
227
244
            checked[rev_info.revision_id] = True
228
245
            add_sha(rev_to_sha, rev_info.revision_id, rev_info.sha1)
229
 
                
 
246
 
230
247
        for (rev, rev_info) in zip(self.real_revisions, self.revisions):
231
248
            add_sha(inv_to_sha, rev_info.revision_id, rev_info.inventory_sha1)
232
249
 
234
251
        missing = {}
235
252
        for revision_id, sha1 in rev_to_sha.iteritems():
236
253
            if repository.has_revision(revision_id):
237
 
                testament = StrictTestament.from_revision(repository, 
 
254
                testament = StrictTestament.from_revision(repository,
238
255
                                                          revision_id)
239
256
                local_sha1 = self._testament_sha1_from_revision(repository,
240
257
                                                                revision_id)
241
258
                if sha1 != local_sha1:
242
 
                    raise BzrError('sha1 mismatch. For revision id {%s}' 
 
259
                    raise BzrError('sha1 mismatch. For revision id {%s}'
243
260
                            'local: %s, bundle: %s' % (revision_id, local_sha1, sha1))
244
261
                else:
245
262
                    count += 1
246
263
            elif revision_id not in checked:
247
264
                missing[revision_id] = sha1
248
265
 
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
 
 
264
266
        if len(missing) > 0:
265
267
            # I don't know if this is an error yet
266
268
            warning('Not all revision hashes could be validated.'
272
274
        """At this point we should have generated the BundleTree,
273
275
        so build up an inventory, and make sure the hashes match.
274
276
        """
275
 
 
276
 
        assert inv is not None
277
 
 
278
277
        # Now we should have a complete inventory entry.
279
278
        s = serializer_v5.write_inventory_to_string(inv)
280
279
        sha1 = sha_string(s)
281
280
        # Target revision is the last entry in the real_revisions list
282
281
        rev = self.get_revision(revision_id)
283
 
        assert rev.revision_id == revision_id
 
282
        if rev.revision_id != revision_id:
 
283
            raise AssertionError()
284
284
        if sha1 != rev.inventory_sha1:
285
 
            open(',,bogus-inv', 'wb').write(s)
 
285
            f = open(',,bogus-inv', 'wb')
 
286
            try:
 
287
                f.write(s)
 
288
            finally:
 
289
                f.close()
286
290
            warning('Inventory sha hash mismatch for revision %s. %s'
287
291
                    ' != %s' % (revision_id, sha1, rev.inventory_sha1))
288
292
 
289
 
    def _validate_revision(self, inventory, revision_id):
 
293
    def _validate_revision(self, tree, revision_id):
290
294
        """Make sure all revision entries match their checksum."""
291
295
 
292
 
        # This is a mapping from each revision id to it's sha hash
 
296
        # This is a mapping from each revision id to its sha hash
293
297
        rev_to_sha1 = {}
294
 
        
 
298
 
295
299
        rev = self.get_revision(revision_id)
296
300
        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)
 
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)
300
306
        if sha1 != rev_info.sha1:
301
307
            raise TestamentMismatch(rev.revision_id, rev_info.sha1, sha1)
302
308
        if rev.revision_id in rev_to_sha1:
329
335
                try:
330
336
                    name, value = info_item.split(':', 1)
331
337
                except ValueError:
332
 
                    raise 'Value %r has no colon' % info_item
 
338
                    raise ValueError('Value %r has no colon' % info_item)
333
339
                if name == 'last-changed':
334
340
                    last_changed = value
335
341
                elif name == 'executable':
336
 
                    assert value in ('yes', 'no'), value
337
342
                    val = (value == 'yes')
338
343
                    bundle_tree.note_executable(new_path, val)
339
344
                elif name == 'target':
343
348
            return last_changed, encoding
344
349
 
345
350
        def do_patch(path, lines, encoding):
346
 
            if encoding is not None:
347
 
                assert encoding == 'base64'
 
351
            if encoding == 'base64':
348
352
                patch = base64.decodestring(''.join(lines))
349
 
            else:
 
353
            elif encoding is None:
350
354
                patch =  ''.join(lines)
 
355
            else:
 
356
                raise ValueError(encoding)
351
357
            bundle_tree.note_patch(path, patch)
352
358
 
353
359
        def renamed(kind, extra, lines):
413
419
            revision = get_rev_id(last_modified, path, kind)
414
420
            if lines:
415
421
                do_patch(path, lines, encoding)
416
 
            
 
422
 
417
423
        valid_actions = {
418
424
            'renamed':renamed,
419
425
            'removed':removed,
442
448
                        ' (unrecognized action): %r' % action_line)
443
449
            valid_actions[action](kind, extra, lines)
444
450
 
445
 
    def install_revisions(self, target_repo):
446
 
        """Install revisions and return the target revision"""
 
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
        """
447
457
        apply_bundle.install_bundle(target_repo, self)
448
458
        return self.target
449
459
 
 
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
 
450
467
 
451
468
class BundleTree(Tree):
 
469
 
452
470
    def __init__(self, base_tree, revision_id):
453
471
        self.base_tree = base_tree
454
472
        self._renamed = {} # Mapping from old_path => new_path
470
488
 
471
489
    def note_rename(self, old_path, new_path):
472
490
        """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
 
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)
475
495
        self._renamed[new_path] = old_path
476
496
        self._renamed_r[old_path] = new_path
477
497
 
507
527
 
508
528
    def old_path(self, new_path):
509
529
        """Get the old_path (path in the base_tree) for the file at new_path"""
510
 
        assert new_path[:1] not in ('\\', '/')
 
530
        if new_path[:1] in ('\\', '/'):
 
531
            raise ValueError(new_path)
511
532
        old_path = self._renamed.get(new_path)
512
533
        if old_path is not None:
513
534
            return old_path
527
548
        #renamed_r
528
549
        if old_path in self._renamed_r:
529
550
            return None
530
 
        return old_path 
 
551
        return old_path
531
552
 
532
553
    def new_path(self, old_path):
533
554
        """Get the new_path (path in the target_tree) for the file at old_path
534
555
        in the base tree.
535
556
        """
536
 
        assert old_path[:1] not in ('\\', '/')
 
557
        if old_path[:1] in ('\\', '/'):
 
558
            raise ValueError(old_path)
537
559
        new_path = self._renamed_r.get(old_path)
538
560
        if new_path is not None:
539
561
            return new_path
552
574
        #renamed_r
553
575
        if new_path in self._renamed:
554
576
            return None
555
 
        return new_path 
 
577
        return new_path
 
578
 
 
579
    def get_root_id(self):
 
580
        return self.path2id('')
556
581
 
557
582
    def path2id(self, path):
558
583
        """Return the id of the file present at path in the target tree."""
564
589
            return None
565
590
        if old_path in self.deleted:
566
591
            return None
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)
 
592
        return self.base_tree.path2id(old_path)
571
593
 
572
594
    def id2path(self, file_id):
573
595
        """Return the new path in the target tree of the file with id file_id"""
592
614
                return None
593
615
        new_path = self.id2path(file_id)
594
616
        return self.base_tree.path2id(new_path)
595
 
        
 
617
 
596
618
    def get_file(self, file_id):
597
619
        """Return a file-like object containing the new contents of the
598
620
        file given by file_id.
603
625
        """
604
626
        base_id = self.old_contents_id(file_id)
605
627
        if (base_id is not None and
606
 
            base_id != self.base_tree.inventory.root.file_id):
 
628
            base_id != self.base_tree.get_root_id()):
607
629
            patch_original = self.base_tree.get_file(base_id)
608
630
        else:
609
631
            patch_original = None
610
632
        file_patch = self.patches.get(self.id2path(file_id))
611
633
        if file_patch is None:
612
 
            if (patch_original is None and 
613
 
                self.get_kind(file_id) == 'directory'):
 
634
            if (patch_original is None and
 
635
                self.kind(file_id) == 'directory'):
614
636
                return StringIO()
615
 
            assert patch_original is not None, "None: %s" % file_id
 
637
            if patch_original is None:
 
638
                raise AssertionError("None: %s" % file_id)
616
639
            return patch_original
617
640
 
618
 
        assert not file_patch.startswith('\\'), \
619
 
            'Malformed patch for %s, %r' % (file_id, file_patch)
 
641
        if file_patch.startswith('\\'):
 
642
            raise ValueError(
 
643
                'Malformed patch for %s, %r' % (file_id, file_patch))
620
644
        return patched_file(file_patch, patch_original)
621
645
 
622
 
    def get_symlink_target(self, file_id):
623
 
        new_path = self.id2path(file_id)
 
646
    def get_symlink_target(self, file_id, path=None):
 
647
        if path is None:
 
648
            path = self.id2path(file_id)
624
649
        try:
625
 
            return self._targets[new_path]
 
650
            return self._targets[path]
626
651
        except KeyError:
627
652
            return self.base_tree.get_symlink_target(file_id)
628
653
 
629
 
    def get_kind(self, file_id):
 
654
    def kind(self, file_id):
630
655
        if file_id in self._kinds:
631
656
            return self._kinds[file_id]
632
 
        return self.base_tree.inventory[file_id].kind
 
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)
633
665
 
634
666
    def is_executable(self, file_id):
635
667
        path = self.id2path(file_id)
636
668
        if path in self._executable:
637
669
            return self._executable[path]
638
670
        else:
639
 
            return self.base_tree.inventory[file_id].executable
 
671
            return self.base_tree.is_executable(file_id)
640
672
 
641
673
    def get_last_changed(self, file_id):
642
674
        path = self.id2path(file_id)
643
675
        if path in self._last_changed:
644
676
            return self._last_changed[path]
645
 
        return self.base_tree.inventory[file_id].revision
 
677
        return self.base_tree.get_file_revision(file_id)
646
678
 
647
679
    def get_size_and_sha1(self, file_id):
648
680
        """Return the size and sha1 hash of the given file id.
669
701
        This need to be called before ever accessing self.inventory
670
702
        """
671
703
        from os.path import dirname, basename
672
 
 
673
 
        assert self.base_tree is not None
674
704
        base_inv = self.base_tree.inventory
675
705
        inv = Inventory(None, self.revision_id)
676
706
 
684
714
                parent_path = dirname(path)
685
715
                parent_id = self.path2id(parent_path)
686
716
 
687
 
            kind = self.get_kind(file_id)
 
717
            kind = self.kind(file_id)
688
718
            revision_id = self.get_last_changed(file_id)
689
719
 
690
720
            name = basename(path)
695
725
                ie.executable = self.is_executable(file_id)
696
726
            elif kind == 'symlink':
697
727
                ie = InventoryLink(file_id, name, parent_id)
698
 
                ie.symlink_target = self.get_symlink_target(file_id)
 
728
                ie.symlink_target = self.get_symlink_target(file_id, path)
699
729
            ie.revision = revision_id
700
730
 
701
 
            if kind in ('directory', 'symlink'):
702
 
                ie.text_size, ie.text_sha1 = None, None
703
 
            else:
 
731
            if kind == 'file':
704
732
                ie.text_size, ie.text_sha1 = self.get_size_and_sha1(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)
 
733
                if ie.text_size is None:
 
734
                    raise BzrError(
 
735
                        'Got a text_size of None for file_id %r' % file_id)
707
736
            inv.add(ie)
708
737
 
709
738
        sorted_entries = self.sorted_path_id()
723
752
        for path, entry in self.inventory.iter_entries():
724
753
            yield entry.file_id
725
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
 
771
 
726
772
    def sorted_path_id(self):
727
773
        paths = []
728
774
        for result in self._new_id.iteritems():
729
775
            paths.append(result)
730
 
        for id in self.base_tree:
 
776
        for id in self.base_tree.all_file_ids():
731
777
            path = self.id2path(id)
732
778
            if path is None:
733
779
                continue