~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/bundle/bundle_data.py

  • Committer: John Arbash Meinel
  • Date: 2006-09-20 14:51:03 UTC
  • mfrom: (0.8.23 version_info)
  • mto: This revision was merged to the branch mainline in revision 2028.
  • Revision ID: john@arbash-meinel.com-20060920145103-02725c6d6c886040
[merge] version-info plugin, and cleanup for layout in bzr

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005-2010 Canonical Ltd
 
1
# Copyright (C) 2006 by Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
12
12
#
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
 
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
17
17
"""Read in a bundle stream, and process it into a BundleReader object."""
18
18
 
21
21
import os
22
22
import pprint
23
23
 
24
 
from bzrlib import (
25
 
    osutils,
26
 
    timestamp,
27
 
    )
28
 
from bzrlib.bundle import apply_bundle
29
 
from bzrlib.errors import (
30
 
    TestamentMismatch,
31
 
    BzrError,
32
 
    )
33
 
from bzrlib.inventory import (
34
 
    Inventory,
35
 
    InventoryDirectory,
36
 
    InventoryFile,
37
 
    InventoryLink,
38
 
    )
39
 
from bzrlib.osutils import sha_string, pathjoin
 
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
40
31
from bzrlib.revision import Revision, NULL_REVISION
41
32
from bzrlib.testament import StrictTestament
42
33
from bzrlib.trace import mutter, warning
 
34
import bzrlib.transport
43
35
from bzrlib.tree import Tree
 
36
import bzrlib.urlutils
44
37
from bzrlib.xml5 import serializer_v5
45
38
 
46
39
 
79
72
        if self.properties:
80
73
            for property in self.properties:
81
74
                key_end = property.find(': ')
82
 
                if key_end == -1:
83
 
                    if not property.endswith(':'):
84
 
                        raise ValueError(property)
85
 
                    key = str(property[:-1])
86
 
                    value = ''
87
 
                else:
88
 
                    key = str(property[:key_end])
89
 
                    value = property[key_end+2:]
 
75
                assert key_end is not None
 
76
                key = property[:key_end].encode('utf-8')
 
77
                value = property[key_end+2:].encode('utf-8')
90
78
                rev.properties[key] = value
91
79
 
92
80
        return rev
93
81
 
94
 
    @staticmethod
95
 
    def from_revision(revision):
96
 
        revision_info = RevisionInfo(revision.revision_id)
97
 
        date = timestamp.format_highres_date(revision.timestamp,
98
 
                                             revision.timezone)
99
 
        revision_info.date = date
100
 
        revision_info.timezone = revision.timezone
101
 
        revision_info.timestamp = revision.timestamp
102
 
        revision_info.message = revision.message.split('\n')
103
 
        revision_info.properties = [': '.join(p) for p in
104
 
                                    revision.properties.iteritems()]
105
 
        return revision_info
106
 
 
107
82
 
108
83
class BundleInfo(object):
109
84
    """This contains the meta information. Stuff that allows you to
110
85
    recreate the revision or inventory XML.
111
86
    """
112
 
    def __init__(self, bundle_format=None):
113
 
        self.bundle_format = None
 
87
    def __init__(self):
114
88
        self.committer = None
115
89
        self.date = None
116
90
        self.message = None
127
101
        self.timestamp = None
128
102
        self.timezone = None
129
103
 
130
 
        # Have we checked the repository yet?
131
 
        self._validated_revisions_against_repo = False
132
 
 
133
104
    def __str__(self):
134
105
        return pprint.pformat(self.__dict__)
135
106
 
138
109
        split up, based on the assumptions that can be made
139
110
        when information is missing.
140
111
        """
141
 
        from bzrlib.timestamp import unpack_highres_date
 
112
        from bzrlib.bundle.serializer import unpack_highres_date
142
113
        # Put in all of the guessable information.
143
114
        if not self.timestamp and self.date:
144
115
            self.timestamp, self.timezone = unpack_highres_date(self.date)
161
132
    def get_base(self, revision):
162
133
        revision_info = self.get_revision_info(revision.revision_id)
163
134
        if revision_info.base_id is not None:
164
 
            return revision_info.base_id
 
135
            if revision_info.base_id == NULL_REVISION:
 
136
                return None
 
137
            else:
 
138
                return revision_info.base_id
165
139
        if len(revision.parent_ids) == 0:
166
140
            # There is no base listed, and
167
141
            # the lowest revision doesn't have a parent
168
142
            # so this is probably against the empty tree
169
 
            # and thus base truly is NULL_REVISION
170
 
            return NULL_REVISION
 
143
            # and thus base truly is None
 
144
            return None
171
145
        else:
172
146
            return revision.parent_ids[-1]
173
147
 
196
170
    def revision_tree(self, repository, revision_id, base=None):
197
171
        revision = self.get_revision(revision_id)
198
172
        base = self.get_base(revision)
199
 
        if base == revision_id:
200
 
            raise AssertionError()
201
 
        if not self._validated_revisions_against_repo:
202
 
            self._validate_references_from_repository(repository)
 
173
        assert base != revision_id
 
174
        self._validate_references_from_repository(repository)
203
175
        revision_info = self.get_revision_info(revision_id)
204
176
        inventory_revision_id = revision_id
205
 
        bundle_tree = BundleTree(repository.revision_tree(base),
 
177
        bundle_tree = BundleTree(repository.revision_tree(base), 
206
178
                                  inventory_revision_id)
207
179
        self._update_tree(bundle_tree, revision_id)
208
180
 
209
181
        inv = bundle_tree.inventory
210
182
        self._validate_inventory(inv, revision_id)
211
 
        self._validate_revision(bundle_tree, revision_id)
 
183
        self._validate_revision(inv, revision_id)
212
184
 
213
185
        return bundle_tree
214
186
 
241
213
        for rev_info in self.revisions:
242
214
            checked[rev_info.revision_id] = True
243
215
            add_sha(rev_to_sha, rev_info.revision_id, rev_info.sha1)
244
 
 
 
216
                
245
217
        for (rev, rev_info) in zip(self.real_revisions, self.revisions):
246
218
            add_sha(inv_to_sha, rev_info.revision_id, rev_info.inventory_sha1)
247
219
 
249
221
        missing = {}
250
222
        for revision_id, sha1 in rev_to_sha.iteritems():
251
223
            if repository.has_revision(revision_id):
252
 
                testament = StrictTestament.from_revision(repository,
 
224
                testament = StrictTestament.from_revision(repository, 
253
225
                                                          revision_id)
254
 
                local_sha1 = self._testament_sha1_from_revision(repository,
255
 
                                                                revision_id)
 
226
                local_sha1 = testament.as_sha1()
256
227
                if sha1 != local_sha1:
257
 
                    raise BzrError('sha1 mismatch. For revision id {%s}'
 
228
                    raise BzrError('sha1 mismatch. For revision id {%s}' 
258
229
                            'local: %s, bundle: %s' % (revision_id, local_sha1, sha1))
259
230
                else:
260
231
                    count += 1
261
232
            elif revision_id not in checked:
262
233
                missing[revision_id] = sha1
263
234
 
 
235
        for inv_id, sha1 in inv_to_sha.iteritems():
 
236
            if repository.has_revision(inv_id):
 
237
                # Note: branch.get_inventory_sha1() just returns the value that
 
238
                # is stored in the revision text, and that value may be out
 
239
                # of date. This is bogus, because that means we aren't
 
240
                # validating the actual text, just that we wrote and read the
 
241
                # string. But for now, what the hell.
 
242
                local_sha1 = repository.get_inventory_sha1(inv_id)
 
243
                if sha1 != local_sha1:
 
244
                    raise BzrError('sha1 mismatch. For inventory id {%s}' 
 
245
                                   'local: %s, bundle: %s' % 
 
246
                                   (inv_id, local_sha1, sha1))
 
247
                else:
 
248
                    count += 1
 
249
 
264
250
        if len(missing) > 0:
265
251
            # I don't know if this is an error yet
266
252
            warning('Not all revision hashes could be validated.'
267
253
                    ' Unable validate %d hashes' % len(missing))
268
254
        mutter('Verified %d sha hashes for the bundle.' % count)
269
 
        self._validated_revisions_against_repo = True
270
255
 
271
256
    def _validate_inventory(self, inv, revision_id):
272
257
        """At this point we should have generated the BundleTree,
273
258
        so build up an inventory, and make sure the hashes match.
274
259
        """
 
260
 
 
261
        assert inv is not None
 
262
 
275
263
        # Now we should have a complete inventory entry.
276
264
        s = serializer_v5.write_inventory_to_string(inv)
277
265
        sha1 = sha_string(s)
278
266
        # Target revision is the last entry in the real_revisions list
279
267
        rev = self.get_revision(revision_id)
280
 
        if rev.revision_id != revision_id:
281
 
            raise AssertionError()
 
268
        assert rev.revision_id == revision_id
282
269
        if sha1 != rev.inventory_sha1:
283
 
            f = open(',,bogus-inv', 'wb')
284
 
            try:
285
 
                f.write(s)
286
 
            finally:
287
 
                f.close()
 
270
            open(',,bogus-inv', 'wb').write(s)
288
271
            warning('Inventory sha hash mismatch for revision %s. %s'
289
272
                    ' != %s' % (revision_id, sha1, rev.inventory_sha1))
290
273
 
291
 
    def _validate_revision(self, tree, revision_id):
 
274
    def _validate_revision(self, inventory, revision_id):
292
275
        """Make sure all revision entries match their checksum."""
293
276
 
294
 
        # This is a mapping from each revision id to its sha hash
 
277
        # This is a mapping from each revision id to it's sha hash
295
278
        rev_to_sha1 = {}
296
 
 
 
279
        
297
280
        rev = self.get_revision(revision_id)
298
281
        rev_info = self.get_revision_info(revision_id)
299
 
        if not (rev.revision_id == rev_info.revision_id):
300
 
            raise AssertionError()
301
 
        if not (rev.revision_id == revision_id):
302
 
            raise AssertionError()
303
 
        sha1 = self._testament_sha1(rev, tree)
 
282
        assert rev.revision_id == rev_info.revision_id
 
283
        assert rev.revision_id == revision_id
 
284
        sha1 = StrictTestament(rev, inventory).as_sha1()
304
285
        if sha1 != rev_info.sha1:
305
286
            raise TestamentMismatch(rev.revision_id, rev_info.sha1, sha1)
306
287
        if rev.revision_id in rev_to_sha1:
317
298
 
318
299
        def get_rev_id(last_changed, path, kind):
319
300
            if last_changed is not None:
320
 
                # last_changed will be a Unicode string because of how it was
321
 
                # read. Convert it back to utf8.
322
 
                changed_revision_id = osutils.safe_revision_id(last_changed,
323
 
                                                               warn=False)
 
301
                changed_revision_id = last_changed.decode('utf-8')
324
302
            else:
325
303
                changed_revision_id = revision_id
326
304
            bundle_tree.note_last_changed(path, changed_revision_id)
333
311
                try:
334
312
                    name, value = info_item.split(':', 1)
335
313
                except ValueError:
336
 
                    raise ValueError('Value %r has no colon' % info_item)
 
314
                    raise 'Value %r has no colon' % info_item
337
315
                if name == 'last-changed':
338
316
                    last_changed = value
339
317
                elif name == 'executable':
 
318
                    assert value in ('yes', 'no'), value
340
319
                    val = (value == 'yes')
341
320
                    bundle_tree.note_executable(new_path, val)
342
321
                elif name == 'target':
346
325
            return last_changed, encoding
347
326
 
348
327
        def do_patch(path, lines, encoding):
349
 
            if encoding == 'base64':
 
328
            if encoding is not None:
 
329
                assert encoding == 'base64'
350
330
                patch = base64.decodestring(''.join(lines))
351
 
            elif encoding is None:
 
331
            else:
352
332
                patch =  ''.join(lines)
353
 
            else:
354
 
                raise ValueError(encoding)
355
333
            bundle_tree.note_patch(path, patch)
356
334
 
357
335
        def renamed(kind, extra, lines):
393
371
            if not info[1].startswith('file-id:'):
394
372
                raise BzrError('The file-id should follow the path for an add'
395
373
                        ': %r' % extra)
396
 
            # This will be Unicode because of how the stream is read. Turn it
397
 
            # back into a utf8 file_id
398
 
            file_id = osutils.safe_file_id(info[1][8:], warn=False)
 
374
            file_id = info[1][8:]
399
375
 
400
376
            bundle_tree.note_id(file_id, path, kind)
401
377
            # this will be overridden in extra_info if executable is specified.
417
393
            revision = get_rev_id(last_modified, path, kind)
418
394
            if lines:
419
395
                do_patch(path, lines, encoding)
420
 
 
 
396
            
421
397
        valid_actions = {
422
398
            'renamed':renamed,
423
399
            'removed':removed,
446
422
                        ' (unrecognized action): %r' % action_line)
447
423
            valid_actions[action](kind, extra, lines)
448
424
 
449
 
    def install_revisions(self, target_repo, stream_input=True):
450
 
        """Install revisions and return the target revision
451
 
 
452
 
        :param target_repo: The repository to install into
453
 
        :param stream_input: Ignored by this implementation.
454
 
        """
455
 
        apply_bundle.install_bundle(target_repo, self)
456
 
        return self.target
457
 
 
458
 
    def get_merge_request(self, target_repo):
459
 
        """Provide data for performing a merge
460
 
 
461
 
        Returns suggested base, suggested target, and patch verification status
462
 
        """
463
 
        return None, self.target, 'inapplicable'
464
 
 
465
425
 
466
426
class BundleTree(Tree):
467
 
 
468
427
    def __init__(self, base_tree, revision_id):
469
428
        self.base_tree = base_tree
470
429
        self._renamed = {} # Mapping from old_path => new_path
486
445
 
487
446
    def note_rename(self, old_path, new_path):
488
447
        """A file/directory has been renamed from old_path => new_path"""
489
 
        if new_path in self._renamed:
490
 
            raise AssertionError(new_path)
491
 
        if old_path in self._renamed_r:
492
 
            raise AssertionError(old_path)
 
448
        assert new_path not in self._renamed
 
449
        assert old_path not in self._renamed_r
493
450
        self._renamed[new_path] = old_path
494
451
        self._renamed_r[old_path] = new_path
495
452
 
525
482
 
526
483
    def old_path(self, new_path):
527
484
        """Get the old_path (path in the base_tree) for the file at new_path"""
528
 
        if new_path[:1] in ('\\', '/'):
529
 
            raise ValueError(new_path)
 
485
        assert new_path[:1] not in ('\\', '/')
530
486
        old_path = self._renamed.get(new_path)
531
487
        if old_path is not None:
532
488
            return old_path
546
502
        #renamed_r
547
503
        if old_path in self._renamed_r:
548
504
            return None
549
 
        return old_path
 
505
        return old_path 
550
506
 
551
507
    def new_path(self, old_path):
552
508
        """Get the new_path (path in the target_tree) for the file at old_path
553
509
        in the base tree.
554
510
        """
555
 
        if old_path[:1] in ('\\', '/'):
556
 
            raise ValueError(old_path)
 
511
        assert old_path[:1] not in ('\\', '/')
557
512
        new_path = self._renamed_r.get(old_path)
558
513
        if new_path is not None:
559
514
            return new_path
572
527
        #renamed_r
573
528
        if new_path in self._renamed:
574
529
            return None
575
 
        return new_path
 
530
        return new_path 
576
531
 
577
532
    def path2id(self, path):
578
533
        """Return the id of the file present at path in the target tree."""
612
567
                return None
613
568
        new_path = self.id2path(file_id)
614
569
        return self.base_tree.path2id(new_path)
615
 
 
 
570
        
616
571
    def get_file(self, file_id):
617
572
        """Return a file-like object containing the new contents of the
618
573
        file given by file_id.
622
577
                then be cached.
623
578
        """
624
579
        base_id = self.old_contents_id(file_id)
625
 
        if (base_id is not None and
626
 
            base_id != self.base_tree.inventory.root.file_id):
 
580
        if base_id is not None:
627
581
            patch_original = self.base_tree.get_file(base_id)
628
582
        else:
629
583
            patch_original = None
630
584
        file_patch = self.patches.get(self.id2path(file_id))
631
585
        if file_patch is None:
632
 
            if (patch_original is None and
 
586
            if (patch_original is None and 
633
587
                self.get_kind(file_id) == 'directory'):
634
588
                return StringIO()
635
 
            if patch_original is None:
636
 
                raise AssertionError("None: %s" % file_id)
 
589
            assert patch_original is not None, "None: %s" % file_id
637
590
            return patch_original
638
591
 
639
 
        if file_patch.startswith('\\'):
640
 
            raise ValueError(
641
 
                'Malformed patch for %s, %r' % (file_id, file_patch))
 
592
        assert not file_patch.startswith('\\'), \
 
593
            'Malformed patch for %s, %r' % (file_id, file_patch)
642
594
        return patched_file(file_patch, patch_original)
643
595
 
644
596
    def get_symlink_target(self, file_id):
664
616
        path = self.id2path(file_id)
665
617
        if path in self._last_changed:
666
618
            return self._last_changed[path]
667
 
        return self.base_tree.get_file_revision(file_id)
 
619
        return self.base_tree.inventory[file_id].revision
668
620
 
669
621
    def get_size_and_sha1(self, file_id):
670
622
        """Return the size and sha1 hash of the given file id.
691
643
        This need to be called before ever accessing self.inventory
692
644
        """
693
645
        from os.path import dirname, basename
 
646
 
 
647
        assert self.base_tree is not None
694
648
        base_inv = self.base_tree.inventory
695
 
        inv = Inventory(None, self.revision_id)
 
649
        root_id = base_inv.root.file_id
 
650
        try:
 
651
            # New inventories have a unique root_id
 
652
            inv = Inventory(root_id, self.revision_id)
 
653
        except TypeError:
 
654
            inv = Inventory(revision_id=self.revision_id)
 
655
        inv.root.revision = self.get_last_changed(root_id)
696
656
 
697
657
        def add_entry(file_id):
698
658
            path = self.id2path(file_id)
699
659
            if path is None:
700
660
                return
701
 
            if path == '':
702
 
                parent_id = None
 
661
            parent_path = dirname(path)
 
662
            if parent_path == u'':
 
663
                parent_id = root_id
703
664
            else:
704
 
                parent_path = dirname(path)
705
665
                parent_id = self.path2id(parent_path)
706
666
 
707
667
            kind = self.get_kind(file_id)
718
678
                ie.symlink_target = self.get_symlink_target(file_id)
719
679
            ie.revision = revision_id
720
680
 
721
 
            if kind == 'file':
 
681
            if kind in ('directory', 'symlink'):
 
682
                ie.text_size, ie.text_sha1 = None, None
 
683
            else:
722
684
                ie.text_size, ie.text_sha1 = self.get_size_and_sha1(file_id)
723
 
                if ie.text_size is None:
724
 
                    raise BzrError(
725
 
                        'Got a text_size of None for file_id %r' % file_id)
 
685
            if (ie.text_size is None) and (kind == 'file'):
 
686
                raise BzrError('Got a text_size of None for file_id %r' % file_id)
726
687
            inv.add(ie)
727
688
 
728
689
        sorted_entries = self.sorted_path_id()
729
690
        for path, file_id in sorted_entries:
 
691
            if file_id == inv.root.file_id:
 
692
                continue
730
693
            add_entry(file_id)
731
694
 
732
695
        return inv
742
705
        for path, entry in self.inventory.iter_entries():
743
706
            yield entry.file_id
744
707
 
745
 
    def list_files(self, include_root=False, from_dir=None, recursive=True):
746
 
        # The only files returned by this are those from the version
747
 
        inv = self.inventory
748
 
        if from_dir is None:
749
 
            from_dir_id = None
750
 
        else:
751
 
            from_dir_id = inv.path2id(from_dir)
752
 
            if from_dir_id is None:
753
 
                # Directory not versioned
754
 
                return
755
 
        entries = inv.iter_entries(from_dir=from_dir_id, recursive=recursive)
756
 
        if inv.root is not None and not include_root and from_dir is None:
757
 
            # skip the root for compatability with the current apis.
758
 
            entries.next()
759
 
        for path, entry in entries:
760
 
            yield path, 'V', entry.kind, entry.file_id, entry
761
 
 
762
708
    def sorted_path_id(self):
763
709
        paths = []
764
710
        for result in self._new_id.iteritems():