~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/bundle/bundle_data.py

  • Committer: Patch Queue Manager
  • Date: 2014-02-12 18:22:22 UTC
  • mfrom: (6589.2.1 trunk)
  • Revision ID: pqm@pqm.ubuntu.com-20140212182222-beouo25gaf1cny76
(vila) The XDG Base Directory Specification uses the XDG_CACHE_HOME,
 not XDG_CACHE_DIR. (Andrew Starr-Bochicchio)

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
 
173
198
    def revision_tree(self, repository, revision_id, base=None):
174
199
        revision = self.get_revision(revision_id)
175
200
        base = self.get_base(revision)
176
 
        assert base != revision_id
177
 
        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)
178
205
        revision_info = self.get_revision_info(revision_id)
179
206
        inventory_revision_id = revision_id
180
 
        bundle_tree = BundleTree(repository.revision_tree(base), 
 
207
        bundle_tree = BundleTree(repository.revision_tree(base),
181
208
                                  inventory_revision_id)
182
209
        self._update_tree(bundle_tree, revision_id)
183
210
 
184
211
        inv = bundle_tree.inventory
185
212
        self._validate_inventory(inv, revision_id)
186
 
        self._validate_revision(inv, revision_id)
 
213
        self._validate_revision(bundle_tree, revision_id)
187
214
 
188
215
        return bundle_tree
189
216
 
216
243
        for rev_info in self.revisions:
217
244
            checked[rev_info.revision_id] = True
218
245
            add_sha(rev_to_sha, rev_info.revision_id, rev_info.sha1)
219
 
                
 
246
 
220
247
        for (rev, rev_info) in zip(self.real_revisions, self.revisions):
221
248
            add_sha(inv_to_sha, rev_info.revision_id, rev_info.inventory_sha1)
222
249
 
224
251
        missing = {}
225
252
        for revision_id, sha1 in rev_to_sha.iteritems():
226
253
            if repository.has_revision(revision_id):
227
 
                testament = StrictTestament.from_revision(repository, 
 
254
                testament = StrictTestament.from_revision(repository,
228
255
                                                          revision_id)
229
256
                local_sha1 = self._testament_sha1_from_revision(repository,
230
257
                                                                revision_id)
231
258
                if sha1 != local_sha1:
232
 
                    raise BzrError('sha1 mismatch. For revision id {%s}' 
 
259
                    raise BzrError('sha1 mismatch. For revision id {%s}'
233
260
                            'local: %s, bundle: %s' % (revision_id, local_sha1, sha1))
234
261
                else:
235
262
                    count += 1
236
263
            elif revision_id not in checked:
237
264
                missing[revision_id] = sha1
238
265
 
239
 
        for inv_id, sha1 in inv_to_sha.iteritems():
240
 
            if repository.has_revision(inv_id):
241
 
                # Note: branch.get_inventory_sha1() just returns the value that
242
 
                # is stored in the revision text, and that value may be out
243
 
                # of date. This is bogus, because that means we aren't
244
 
                # validating the actual text, just that we wrote and read the
245
 
                # string. But for now, what the hell.
246
 
                local_sha1 = repository.get_inventory_sha1(inv_id)
247
 
                if sha1 != local_sha1:
248
 
                    raise BzrError('sha1 mismatch. For inventory id {%s}' 
249
 
                                   'local: %s, bundle: %s' % 
250
 
                                   (inv_id, local_sha1, sha1))
251
 
                else:
252
 
                    count += 1
253
 
 
254
266
        if len(missing) > 0:
255
267
            # I don't know if this is an error yet
256
268
            warning('Not all revision hashes could be validated.'
257
269
                    ' Unable validate %d hashes' % len(missing))
258
270
        mutter('Verified %d sha hashes for the bundle.' % count)
 
271
        self._validated_revisions_against_repo = True
259
272
 
260
273
    def _validate_inventory(self, inv, revision_id):
261
274
        """At this point we should have generated the BundleTree,
262
275
        so build up an inventory, and make sure the hashes match.
263
276
        """
264
 
 
265
 
        assert inv is not None
266
 
 
267
277
        # Now we should have a complete inventory entry.
268
278
        s = serializer_v5.write_inventory_to_string(inv)
269
279
        sha1 = sha_string(s)
270
280
        # Target revision is the last entry in the real_revisions list
271
281
        rev = self.get_revision(revision_id)
272
 
        assert rev.revision_id == revision_id
 
282
        if rev.revision_id != revision_id:
 
283
            raise AssertionError()
273
284
        if sha1 != rev.inventory_sha1:
274
 
            open(',,bogus-inv', 'wb').write(s)
 
285
            f = open(',,bogus-inv', 'wb')
 
286
            try:
 
287
                f.write(s)
 
288
            finally:
 
289
                f.close()
275
290
            warning('Inventory sha hash mismatch for revision %s. %s'
276
291
                    ' != %s' % (revision_id, sha1, rev.inventory_sha1))
277
292
 
278
 
    def _validate_revision(self, inventory, revision_id):
 
293
    def _validate_revision(self, tree, revision_id):
279
294
        """Make sure all revision entries match their checksum."""
280
295
 
281
 
        # 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
282
297
        rev_to_sha1 = {}
283
 
        
 
298
 
284
299
        rev = self.get_revision(revision_id)
285
300
        rev_info = self.get_revision_info(revision_id)
286
 
        assert rev.revision_id == rev_info.revision_id
287
 
        assert rev.revision_id == revision_id
288
 
        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)
289
306
        if sha1 != rev_info.sha1:
290
307
            raise TestamentMismatch(rev.revision_id, rev_info.sha1, sha1)
291
308
        if rev.revision_id in rev_to_sha1:
302
319
 
303
320
        def get_rev_id(last_changed, path, kind):
304
321
            if last_changed is not None:
305
 
                changed_revision_id = osutils.safe_revision_id(last_changed)
 
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)
306
326
            else:
307
327
                changed_revision_id = revision_id
308
328
            bundle_tree.note_last_changed(path, changed_revision_id)
315
335
                try:
316
336
                    name, value = info_item.split(':', 1)
317
337
                except ValueError:
318
 
                    raise 'Value %r has no colon' % info_item
 
338
                    raise ValueError('Value %r has no colon' % info_item)
319
339
                if name == 'last-changed':
320
340
                    last_changed = value
321
341
                elif name == 'executable':
322
 
                    assert value in ('yes', 'no'), value
323
342
                    val = (value == 'yes')
324
343
                    bundle_tree.note_executable(new_path, val)
325
344
                elif name == 'target':
329
348
            return last_changed, encoding
330
349
 
331
350
        def do_patch(path, lines, encoding):
332
 
            if encoding is not None:
333
 
                assert encoding == 'base64'
 
351
            if encoding == 'base64':
334
352
                patch = base64.decodestring(''.join(lines))
335
 
            else:
 
353
            elif encoding is None:
336
354
                patch =  ''.join(lines)
 
355
            else:
 
356
                raise ValueError(encoding)
337
357
            bundle_tree.note_patch(path, patch)
338
358
 
339
359
        def renamed(kind, extra, lines):
377
397
                        ': %r' % extra)
378
398
            # This will be Unicode because of how the stream is read. Turn it
379
399
            # back into a utf8 file_id
380
 
            file_id = osutils.safe_file_id(info[1][8:])
 
400
            file_id = osutils.safe_file_id(info[1][8:], warn=False)
381
401
 
382
402
            bundle_tree.note_id(file_id, path, kind)
383
403
            # this will be overridden in extra_info if executable is specified.
399
419
            revision = get_rev_id(last_modified, path, kind)
400
420
            if lines:
401
421
                do_patch(path, lines, encoding)
402
 
            
 
422
 
403
423
        valid_actions = {
404
424
            'renamed':renamed,
405
425
            'removed':removed,
428
448
                        ' (unrecognized action): %r' % action_line)
429
449
            valid_actions[action](kind, extra, lines)
430
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
 
431
467
 
432
468
class BundleTree(Tree):
 
469
 
433
470
    def __init__(self, base_tree, revision_id):
434
471
        self.base_tree = base_tree
435
472
        self._renamed = {} # Mapping from old_path => new_path
451
488
 
452
489
    def note_rename(self, old_path, new_path):
453
490
        """A file/directory has been renamed from old_path => new_path"""
454
 
        assert new_path not in self._renamed
455
 
        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)
456
495
        self._renamed[new_path] = old_path
457
496
        self._renamed_r[old_path] = new_path
458
497
 
488
527
 
489
528
    def old_path(self, new_path):
490
529
        """Get the old_path (path in the base_tree) for the file at new_path"""
491
 
        assert new_path[:1] not in ('\\', '/')
 
530
        if new_path[:1] in ('\\', '/'):
 
531
            raise ValueError(new_path)
492
532
        old_path = self._renamed.get(new_path)
493
533
        if old_path is not None:
494
534
            return old_path
508
548
        #renamed_r
509
549
        if old_path in self._renamed_r:
510
550
            return None
511
 
        return old_path 
 
551
        return old_path
512
552
 
513
553
    def new_path(self, old_path):
514
554
        """Get the new_path (path in the target_tree) for the file at old_path
515
555
        in the base tree.
516
556
        """
517
 
        assert old_path[:1] not in ('\\', '/')
 
557
        if old_path[:1] in ('\\', '/'):
 
558
            raise ValueError(old_path)
518
559
        new_path = self._renamed_r.get(old_path)
519
560
        if new_path is not None:
520
561
            return new_path
533
574
        #renamed_r
534
575
        if new_path in self._renamed:
535
576
            return None
536
 
        return new_path 
 
577
        return new_path
 
578
 
 
579
    def get_root_id(self):
 
580
        return self.path2id('')
537
581
 
538
582
    def path2id(self, path):
539
583
        """Return the id of the file present at path in the target tree."""
545
589
            return None
546
590
        if old_path in self.deleted:
547
591
            return None
548
 
        if getattr(self.base_tree, 'path2id', None) is not None:
549
 
            return self.base_tree.path2id(old_path)
550
 
        else:
551
 
            return self.base_tree.inventory.path2id(old_path)
 
592
        return self.base_tree.path2id(old_path)
552
593
 
553
594
    def id2path(self, file_id):
554
595
        """Return the new path in the target tree of the file with id file_id"""
573
614
                return None
574
615
        new_path = self.id2path(file_id)
575
616
        return self.base_tree.path2id(new_path)
576
 
        
 
617
 
577
618
    def get_file(self, file_id):
578
619
        """Return a file-like object containing the new contents of the
579
620
        file given by file_id.
584
625
        """
585
626
        base_id = self.old_contents_id(file_id)
586
627
        if (base_id is not None and
587
 
            base_id != self.base_tree.inventory.root.file_id):
 
628
            base_id != self.base_tree.get_root_id()):
588
629
            patch_original = self.base_tree.get_file(base_id)
589
630
        else:
590
631
            patch_original = None
591
632
        file_patch = self.patches.get(self.id2path(file_id))
592
633
        if file_patch is None:
593
 
            if (patch_original is None and 
594
 
                self.get_kind(file_id) == 'directory'):
 
634
            if (patch_original is None and
 
635
                self.kind(file_id) == 'directory'):
595
636
                return StringIO()
596
 
            assert patch_original is not None, "None: %s" % file_id
 
637
            if patch_original is None:
 
638
                raise AssertionError("None: %s" % file_id)
597
639
            return patch_original
598
640
 
599
 
        assert not file_patch.startswith('\\'), \
600
 
            '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))
601
644
        return patched_file(file_patch, patch_original)
602
645
 
603
 
    def get_symlink_target(self, file_id):
604
 
        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)
605
649
        try:
606
 
            return self._targets[new_path]
 
650
            return self._targets[path]
607
651
        except KeyError:
608
652
            return self.base_tree.get_symlink_target(file_id)
609
653
 
610
 
    def get_kind(self, file_id):
 
654
    def kind(self, file_id):
611
655
        if file_id in self._kinds:
612
656
            return self._kinds[file_id]
613
 
        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)
614
665
 
615
666
    def is_executable(self, file_id):
616
667
        path = self.id2path(file_id)
617
668
        if path in self._executable:
618
669
            return self._executable[path]
619
670
        else:
620
 
            return self.base_tree.inventory[file_id].executable
 
671
            return self.base_tree.is_executable(file_id)
621
672
 
622
673
    def get_last_changed(self, file_id):
623
674
        path = self.id2path(file_id)
624
675
        if path in self._last_changed:
625
676
            return self._last_changed[path]
626
 
        return self.base_tree.inventory[file_id].revision
 
677
        return self.base_tree.get_file_revision(file_id)
627
678
 
628
679
    def get_size_and_sha1(self, file_id):
629
680
        """Return the size and sha1 hash of the given file id.
636
687
        if new_path not in self.patches:
637
688
            # If the entry does not have a patch, then the
638
689
            # contents must be the same as in the base_tree
639
 
            ie = self.base_tree.inventory[file_id]
640
 
            if ie.text_size is None:
641
 
                return ie.text_size, ie.text_sha1
642
 
            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
643
693
        fileobj = self.get_file(file_id)
644
694
        content = fileobj.read()
645
695
        return len(content), sha_string(content)
650
700
        This need to be called before ever accessing self.inventory
651
701
        """
652
702
        from os.path import dirname, basename
653
 
 
654
 
        assert self.base_tree is not None
655
 
        base_inv = self.base_tree.inventory
656
703
        inv = Inventory(None, self.revision_id)
657
704
 
658
705
        def add_entry(file_id):
665
712
                parent_path = dirname(path)
666
713
                parent_id = self.path2id(parent_path)
667
714
 
668
 
            kind = self.get_kind(file_id)
 
715
            kind = self.kind(file_id)
669
716
            revision_id = self.get_last_changed(file_id)
670
717
 
671
718
            name = basename(path)
676
723
                ie.executable = self.is_executable(file_id)
677
724
            elif kind == 'symlink':
678
725
                ie = InventoryLink(file_id, name, parent_id)
679
 
                ie.symlink_target = self.get_symlink_target(file_id)
 
726
                ie.symlink_target = self.get_symlink_target(file_id, path)
680
727
            ie.revision = revision_id
681
728
 
682
 
            if kind in ('directory', 'symlink'):
683
 
                ie.text_size, ie.text_sha1 = None, None
684
 
            else:
 
729
            if kind == 'file':
685
730
                ie.text_size, ie.text_sha1 = self.get_size_and_sha1(file_id)
686
 
            if (ie.text_size is None) and (kind == 'file'):
687
 
                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)
688
734
            inv.add(ie)
689
735
 
690
736
        sorted_entries = self.sorted_path_id()
700
746
    # at that instant
701
747
    inventory = property(_get_inventory)
702
748
 
703
 
    def __iter__(self):
704
 
        for path, entry in self.inventory.iter_entries():
705
 
            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
706
771
 
707
772
    def sorted_path_id(self):
708
773
        paths = []
709
774
        for result in self._new_id.iteritems():
710
775
            paths.append(result)
711
 
        for id in self.base_tree:
 
776
        for id in self.base_tree.all_file_ids():
712
777
            path = self.id2path(id)
713
778
            if path is None:
714
779
                continue