~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/bundle/bundle_data.py

  • Committer: Colin Watson
  • Date: 2015-07-02 10:37:05 UTC
  • mto: This revision was merged to the branch mainline in revision 6605.
  • Revision ID: cjwatson@canonical.com-20150702103705-zdfslk3wb70sz37n
Avoid associating dirty patch headers with the previous file in the patch.

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,
26
 
    )
27
 
import bzrlib.errors
28
 
from bzrlib.errors import (TestamentMismatch, BzrError, 
29
 
                           MalformedHeader, MalformedPatches, NotABundle)
30
 
from bzrlib.inventory import (Inventory, InventoryEntry,
31
 
                              InventoryDirectory, InventoryFile,
32
 
                              InventoryLink)
33
 
from bzrlib.osutils import sha_file, sha_string, pathjoin
 
28
    timestamp,
 
29
    )
 
30
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
34
42
from bzrlib.revision import Revision, NULL_REVISION
35
43
from bzrlib.testament import StrictTestament
36
44
from bzrlib.trace import mutter, warning
37
 
import bzrlib.transport
38
45
from bzrlib.tree import Tree
39
 
import bzrlib.urlutils
40
46
from bzrlib.xml5 import serializer_v5
41
47
 
42
48
 
75
81
        if self.properties:
76
82
            for property in self.properties:
77
83
                key_end = property.find(': ')
78
 
                assert key_end is not None
79
 
                key = property[:key_end].encode('utf-8')
80
 
                value = property[key_end+2:].encode('utf-8')
 
84
                if key_end == -1:
 
85
                    if not property.endswith(':'):
 
86
                        raise ValueError(property)
 
87
                    key = str(property[:-1])
 
88
                    value = ''
 
89
                else:
 
90
                    key = str(property[:key_end])
 
91
                    value = property[key_end+2:]
81
92
                rev.properties[key] = value
82
93
 
83
94
        return rev
84
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
 
85
109
 
86
110
class BundleInfo(object):
87
111
    """This contains the meta information. Stuff that allows you to
88
112
    recreate the revision or inventory XML.
89
113
    """
90
 
    def __init__(self):
 
114
    def __init__(self, bundle_format=None):
 
115
        self.bundle_format = None
91
116
        self.committer = None
92
117
        self.date = None
93
118
        self.message = None
104
129
        self.timestamp = None
105
130
        self.timezone = None
106
131
 
 
132
        # Have we checked the repository yet?
 
133
        self._validated_revisions_against_repo = False
 
134
 
107
135
    def __str__(self):
108
136
        return pprint.pformat(self.__dict__)
109
137
 
112
140
        split up, based on the assumptions that can be made
113
141
        when information is missing.
114
142
        """
115
 
        from bzrlib.bundle.serializer import unpack_highres_date
 
143
        from bzrlib.timestamp import unpack_highres_date
116
144
        # Put in all of the guessable information.
117
145
        if not self.timestamp and self.date:
118
146
            self.timestamp, self.timezone = unpack_highres_date(self.date)
135
163
    def get_base(self, revision):
136
164
        revision_info = self.get_revision_info(revision.revision_id)
137
165
        if revision_info.base_id is not None:
138
 
            if revision_info.base_id == NULL_REVISION:
139
 
                return None
140
 
            else:
141
 
                return revision_info.base_id
 
166
            return revision_info.base_id
142
167
        if len(revision.parent_ids) == 0:
143
168
            # There is no base listed, and
144
169
            # the lowest revision doesn't have a parent
145
170
            # so this is probably against the empty tree
146
 
            # and thus base truly is None
147
 
            return None
 
171
            # and thus base truly is NULL_REVISION
 
172
            return NULL_REVISION
148
173
        else:
149
174
            return revision.parent_ids[-1]
150
175
 
171
196
        raise KeyError(revision_id)
172
197
 
173
198
    def revision_tree(self, repository, revision_id, base=None):
174
 
        revision_id = osutils.safe_revision_id(revision_id)
175
199
        revision = self.get_revision(revision_id)
176
200
        base = self.get_base(revision)
177
 
        assert base != revision_id
178
 
        self._validate_references_from_repository(repository)
 
201
        if base == revision_id:
 
202
            raise AssertionError()
 
203
        if not self._validated_revisions_against_repo:
 
204
            self._validate_references_from_repository(repository)
179
205
        revision_info = self.get_revision_info(revision_id)
180
206
        inventory_revision_id = revision_id
181
 
        bundle_tree = BundleTree(repository.revision_tree(base), 
 
207
        bundle_tree = BundleTree(repository.revision_tree(base),
182
208
                                  inventory_revision_id)
183
209
        self._update_tree(bundle_tree, revision_id)
184
210
 
185
211
        inv = bundle_tree.inventory
186
212
        self._validate_inventory(inv, revision_id)
187
 
        self._validate_revision(inv, revision_id)
 
213
        self._validate_revision(bundle_tree, revision_id)
188
214
 
189
215
        return bundle_tree
190
216
 
217
243
        for rev_info in self.revisions:
218
244
            checked[rev_info.revision_id] = True
219
245
            add_sha(rev_to_sha, rev_info.revision_id, rev_info.sha1)
220
 
                
 
246
 
221
247
        for (rev, rev_info) in zip(self.real_revisions, self.revisions):
222
248
            add_sha(inv_to_sha, rev_info.revision_id, rev_info.inventory_sha1)
223
249
 
225
251
        missing = {}
226
252
        for revision_id, sha1 in rev_to_sha.iteritems():
227
253
            if repository.has_revision(revision_id):
228
 
                testament = StrictTestament.from_revision(repository, 
 
254
                testament = StrictTestament.from_revision(repository,
229
255
                                                          revision_id)
230
256
                local_sha1 = self._testament_sha1_from_revision(repository,
231
257
                                                                revision_id)
232
258
                if sha1 != local_sha1:
233
 
                    raise BzrError('sha1 mismatch. For revision id {%s}' 
 
259
                    raise BzrError('sha1 mismatch. For revision id {%s}'
234
260
                            'local: %s, bundle: %s' % (revision_id, local_sha1, sha1))
235
261
                else:
236
262
                    count += 1
237
263
            elif revision_id not in checked:
238
264
                missing[revision_id] = sha1
239
265
 
240
 
        for inv_id, sha1 in inv_to_sha.iteritems():
241
 
            if repository.has_revision(inv_id):
242
 
                # Note: branch.get_inventory_sha1() just returns the value that
243
 
                # is stored in the revision text, and that value may be out
244
 
                # of date. This is bogus, because that means we aren't
245
 
                # validating the actual text, just that we wrote and read the
246
 
                # string. But for now, what the hell.
247
 
                local_sha1 = repository.get_inventory_sha1(inv_id)
248
 
                if sha1 != local_sha1:
249
 
                    raise BzrError('sha1 mismatch. For inventory id {%s}' 
250
 
                                   'local: %s, bundle: %s' % 
251
 
                                   (inv_id, local_sha1, sha1))
252
 
                else:
253
 
                    count += 1
254
 
 
255
266
        if len(missing) > 0:
256
267
            # I don't know if this is an error yet
257
268
            warning('Not all revision hashes could be validated.'
258
269
                    ' Unable validate %d hashes' % len(missing))
259
270
        mutter('Verified %d sha hashes for the bundle.' % count)
 
271
        self._validated_revisions_against_repo = True
260
272
 
261
273
    def _validate_inventory(self, inv, revision_id):
262
274
        """At this point we should have generated the BundleTree,
263
275
        so build up an inventory, and make sure the hashes match.
264
276
        """
265
 
 
266
 
        assert inv is not None
267
 
 
268
277
        # Now we should have a complete inventory entry.
269
278
        s = serializer_v5.write_inventory_to_string(inv)
270
279
        sha1 = sha_string(s)
271
280
        # Target revision is the last entry in the real_revisions list
272
281
        rev = self.get_revision(revision_id)
273
 
        assert rev.revision_id == revision_id
 
282
        if rev.revision_id != revision_id:
 
283
            raise AssertionError()
274
284
        if sha1 != rev.inventory_sha1:
275
 
            open(',,bogus-inv', 'wb').write(s)
 
285
            f = open(',,bogus-inv', 'wb')
 
286
            try:
 
287
                f.write(s)
 
288
            finally:
 
289
                f.close()
276
290
            warning('Inventory sha hash mismatch for revision %s. %s'
277
291
                    ' != %s' % (revision_id, sha1, rev.inventory_sha1))
278
292
 
279
 
    def _validate_revision(self, inventory, revision_id):
 
293
    def _validate_revision(self, tree, revision_id):
280
294
        """Make sure all revision entries match their checksum."""
281
295
 
282
 
        # 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
283
297
        rev_to_sha1 = {}
284
 
        
 
298
 
285
299
        rev = self.get_revision(revision_id)
286
300
        rev_info = self.get_revision_info(revision_id)
287
 
        assert rev.revision_id == rev_info.revision_id
288
 
        assert rev.revision_id == revision_id
289
 
        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)
290
306
        if sha1 != rev_info.sha1:
291
307
            raise TestamentMismatch(rev.revision_id, rev_info.sha1, sha1)
292
308
        if rev.revision_id in rev_to_sha1:
319
335
                try:
320
336
                    name, value = info_item.split(':', 1)
321
337
                except ValueError:
322
 
                    raise 'Value %r has no colon' % info_item
 
338
                    raise ValueError('Value %r has no colon' % info_item)
323
339
                if name == 'last-changed':
324
340
                    last_changed = value
325
341
                elif name == 'executable':
326
 
                    assert value in ('yes', 'no'), value
327
342
                    val = (value == 'yes')
328
343
                    bundle_tree.note_executable(new_path, val)
329
344
                elif name == 'target':
333
348
            return last_changed, encoding
334
349
 
335
350
        def do_patch(path, lines, encoding):
336
 
            if encoding is not None:
337
 
                assert encoding == 'base64'
 
351
            if encoding == 'base64':
338
352
                patch = base64.decodestring(''.join(lines))
339
 
            else:
 
353
            elif encoding is None:
340
354
                patch =  ''.join(lines)
 
355
            else:
 
356
                raise ValueError(encoding)
341
357
            bundle_tree.note_patch(path, patch)
342
358
 
343
359
        def renamed(kind, extra, lines):
403
419
            revision = get_rev_id(last_modified, path, kind)
404
420
            if lines:
405
421
                do_patch(path, lines, encoding)
406
 
            
 
422
 
407
423
        valid_actions = {
408
424
            'renamed':renamed,
409
425
            'removed':removed,
432
448
                        ' (unrecognized action): %r' % action_line)
433
449
            valid_actions[action](kind, extra, lines)
434
450
 
 
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
        """
 
457
        apply_bundle.install_bundle(target_repo, self)
 
458
        return self.target
 
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
 
435
467
 
436
468
class BundleTree(Tree):
 
469
 
437
470
    def __init__(self, base_tree, revision_id):
438
471
        self.base_tree = base_tree
439
472
        self._renamed = {} # Mapping from old_path => new_path
455
488
 
456
489
    def note_rename(self, old_path, new_path):
457
490
        """A file/directory has been renamed from old_path => new_path"""
458
 
        assert new_path not in self._renamed
459
 
        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)
460
495
        self._renamed[new_path] = old_path
461
496
        self._renamed_r[old_path] = new_path
462
497
 
492
527
 
493
528
    def old_path(self, new_path):
494
529
        """Get the old_path (path in the base_tree) for the file at new_path"""
495
 
        assert new_path[:1] not in ('\\', '/')
 
530
        if new_path[:1] in ('\\', '/'):
 
531
            raise ValueError(new_path)
496
532
        old_path = self._renamed.get(new_path)
497
533
        if old_path is not None:
498
534
            return old_path
512
548
        #renamed_r
513
549
        if old_path in self._renamed_r:
514
550
            return None
515
 
        return old_path 
 
551
        return old_path
516
552
 
517
553
    def new_path(self, old_path):
518
554
        """Get the new_path (path in the target_tree) for the file at old_path
519
555
        in the base tree.
520
556
        """
521
 
        assert old_path[:1] not in ('\\', '/')
 
557
        if old_path[:1] in ('\\', '/'):
 
558
            raise ValueError(old_path)
522
559
        new_path = self._renamed_r.get(old_path)
523
560
        if new_path is not None:
524
561
            return new_path
537
574
        #renamed_r
538
575
        if new_path in self._renamed:
539
576
            return None
540
 
        return new_path 
 
577
        return new_path
 
578
 
 
579
    def get_root_id(self):
 
580
        return self.path2id('')
541
581
 
542
582
    def path2id(self, path):
543
583
        """Return the id of the file present at path in the target tree."""
549
589
            return None
550
590
        if old_path in self.deleted:
551
591
            return None
552
 
        if getattr(self.base_tree, 'path2id', None) is not None:
553
 
            return self.base_tree.path2id(old_path)
554
 
        else:
555
 
            return self.base_tree.inventory.path2id(old_path)
 
592
        return self.base_tree.path2id(old_path)
556
593
 
557
594
    def id2path(self, file_id):
558
595
        """Return the new path in the target tree of the file with id file_id"""
577
614
                return None
578
615
        new_path = self.id2path(file_id)
579
616
        return self.base_tree.path2id(new_path)
580
 
        
 
617
 
581
618
    def get_file(self, file_id):
582
619
        """Return a file-like object containing the new contents of the
583
620
        file given by file_id.
588
625
        """
589
626
        base_id = self.old_contents_id(file_id)
590
627
        if (base_id is not None and
591
 
            base_id != self.base_tree.inventory.root.file_id):
 
628
            base_id != self.base_tree.get_root_id()):
592
629
            patch_original = self.base_tree.get_file(base_id)
593
630
        else:
594
631
            patch_original = None
595
632
        file_patch = self.patches.get(self.id2path(file_id))
596
633
        if file_patch is None:
597
 
            if (patch_original is None and 
598
 
                self.get_kind(file_id) == 'directory'):
 
634
            if (patch_original is None and
 
635
                self.kind(file_id) == 'directory'):
599
636
                return StringIO()
600
 
            assert patch_original is not None, "None: %s" % file_id
 
637
            if patch_original is None:
 
638
                raise AssertionError("None: %s" % file_id)
601
639
            return patch_original
602
640
 
603
 
        assert not file_patch.startswith('\\'), \
604
 
            '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))
605
644
        return patched_file(file_patch, patch_original)
606
645
 
607
 
    def get_symlink_target(self, file_id):
608
 
        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)
609
649
        try:
610
 
            return self._targets[new_path]
 
650
            return self._targets[path]
611
651
        except KeyError:
612
652
            return self.base_tree.get_symlink_target(file_id)
613
653
 
614
 
    def get_kind(self, file_id):
 
654
    def kind(self, file_id):
615
655
        if file_id in self._kinds:
616
656
            return self._kinds[file_id]
617
 
        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)
618
665
 
619
666
    def is_executable(self, file_id):
620
667
        path = self.id2path(file_id)
621
668
        if path in self._executable:
622
669
            return self._executable[path]
623
670
        else:
624
 
            return self.base_tree.inventory[file_id].executable
 
671
            return self.base_tree.is_executable(file_id)
625
672
 
626
673
    def get_last_changed(self, file_id):
627
674
        path = self.id2path(file_id)
628
675
        if path in self._last_changed:
629
676
            return self._last_changed[path]
630
 
        return self.base_tree.inventory[file_id].revision
 
677
        return self.base_tree.get_file_revision(file_id)
631
678
 
632
679
    def get_size_and_sha1(self, file_id):
633
680
        """Return the size and sha1 hash of the given file id.
640
687
        if new_path not in self.patches:
641
688
            # If the entry does not have a patch, then the
642
689
            # contents must be the same as in the base_tree
643
 
            ie = self.base_tree.inventory[file_id]
644
 
            if ie.text_size is None:
645
 
                return ie.text_size, ie.text_sha1
646
 
            return int(ie.text_size), ie.text_sha1
 
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
647
693
        fileobj = self.get_file(file_id)
648
694
        content = fileobj.read()
649
695
        return len(content), sha_string(content)
654
700
        This need to be called before ever accessing self.inventory
655
701
        """
656
702
        from os.path import dirname, basename
657
 
 
658
 
        assert self.base_tree is not None
659
 
        base_inv = self.base_tree.inventory
660
703
        inv = Inventory(None, self.revision_id)
661
704
 
662
705
        def add_entry(file_id):
669
712
                parent_path = dirname(path)
670
713
                parent_id = self.path2id(parent_path)
671
714
 
672
 
            kind = self.get_kind(file_id)
 
715
            kind = self.kind(file_id)
673
716
            revision_id = self.get_last_changed(file_id)
674
717
 
675
718
            name = basename(path)
680
723
                ie.executable = self.is_executable(file_id)
681
724
            elif kind == 'symlink':
682
725
                ie = InventoryLink(file_id, name, parent_id)
683
 
                ie.symlink_target = self.get_symlink_target(file_id)
 
726
                ie.symlink_target = self.get_symlink_target(file_id, path)
684
727
            ie.revision = revision_id
685
728
 
686
 
            if kind in ('directory', 'symlink'):
687
 
                ie.text_size, ie.text_sha1 = None, None
688
 
            else:
 
729
            if kind == 'file':
689
730
                ie.text_size, ie.text_sha1 = self.get_size_and_sha1(file_id)
690
 
            if (ie.text_size is None) and (kind == 'file'):
691
 
                raise BzrError('Got a text_size of None for file_id %r' % 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)
692
734
            inv.add(ie)
693
735
 
694
736
        sorted_entries = self.sorted_path_id()
704
746
    # at that instant
705
747
    inventory = property(_get_inventory)
706
748
 
707
 
    def __iter__(self):
708
 
        for path, entry in self.inventory.iter_entries():
709
 
            yield entry.file_id
 
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
710
771
 
711
772
    def sorted_path_id(self):
712
773
        paths = []
713
774
        for result in self._new_id.iteritems():
714
775
            paths.append(result)
715
 
        for id in self.base_tree:
 
776
        for id in self.base_tree.all_file_ids():
716
777
            path = self.id2path(id)
717
778
            if path is None:
718
779
                continue