~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/bundle/bundle_data.py

  • Committer: Jelmer Vernooij
  • Date: 2015-11-15 02:30:05 UTC
  • mto: This revision was merged to the branch mainline in revision 6609.
  • Revision ID: jelmer@jelmer.uk-20151115023005-fcfi763b5eu1ne2o
Fix auodoc_rstx when running with LANG=C.

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