~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/bundle/bundle_data.py

  • Committer: INADA Naoki
  • Date: 2011-05-05 09:15:34 UTC
  • mto: (5830.3.3 i18n-msgfmt)
  • mto: This revision was merged to the branch mainline in revision 5873.
  • Revision ID: songofacandy@gmail.com-20110505091534-7sv835xpofwrmpt4
Add update-pot command to Makefile and tools/bzrgettext script that
extracts help text from bzr commands.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2006 by 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
 
21
21
import os
22
22
import pprint
23
23
 
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
 
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
31
40
from bzrlib.revision import Revision, NULL_REVISION
32
41
from bzrlib.testament import StrictTestament
33
42
from bzrlib.trace import mutter, warning
34
 
import bzrlib.transport
35
43
from bzrlib.tree import Tree
36
 
import bzrlib.urlutils
37
44
from bzrlib.xml5 import serializer_v5
38
45
 
39
46
 
72
79
        if self.properties:
73
80
            for property in self.properties:
74
81
                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')
 
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:]
78
90
                rev.properties[key] = value
79
91
 
80
92
        return rev
81
93
 
 
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
 
82
107
 
83
108
class BundleInfo(object):
84
109
    """This contains the meta information. Stuff that allows you to
85
110
    recreate the revision or inventory XML.
86
111
    """
87
 
    def __init__(self):
 
112
    def __init__(self, bundle_format=None):
 
113
        self.bundle_format = None
88
114
        self.committer = None
89
115
        self.date = None
90
116
        self.message = None
101
127
        self.timestamp = None
102
128
        self.timezone = None
103
129
 
 
130
        # Have we checked the repository yet?
 
131
        self._validated_revisions_against_repo = False
 
132
 
104
133
    def __str__(self):
105
134
        return pprint.pformat(self.__dict__)
106
135
 
109
138
        split up, based on the assumptions that can be made
110
139
        when information is missing.
111
140
        """
112
 
        from bzrlib.bundle.serializer import unpack_highres_date
 
141
        from bzrlib.timestamp import unpack_highres_date
113
142
        # Put in all of the guessable information.
114
143
        if not self.timestamp and self.date:
115
144
            self.timestamp, self.timezone = unpack_highres_date(self.date)
132
161
    def get_base(self, revision):
133
162
        revision_info = self.get_revision_info(revision.revision_id)
134
163
        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
 
164
            return revision_info.base_id
139
165
        if len(revision.parent_ids) == 0:
140
166
            # There is no base listed, and
141
167
            # the lowest revision doesn't have a parent
142
168
            # so this is probably against the empty tree
143
 
            # and thus base truly is None
144
 
            return None
 
169
            # and thus base truly is NULL_REVISION
 
170
            return NULL_REVISION
145
171
        else:
146
172
            return revision.parent_ids[-1]
147
173
 
170
196
    def revision_tree(self, repository, revision_id, base=None):
171
197
        revision = self.get_revision(revision_id)
172
198
        base = self.get_base(revision)
173
 
        assert base != revision_id
174
 
        self._validate_references_from_repository(repository)
 
199
        if base == revision_id:
 
200
            raise AssertionError()
 
201
        if not self._validated_revisions_against_repo:
 
202
            self._validate_references_from_repository(repository)
175
203
        revision_info = self.get_revision_info(revision_id)
176
204
        inventory_revision_id = revision_id
177
 
        bundle_tree = BundleTree(repository.revision_tree(base), 
 
205
        bundle_tree = BundleTree(repository.revision_tree(base),
178
206
                                  inventory_revision_id)
179
207
        self._update_tree(bundle_tree, revision_id)
180
208
 
181
209
        inv = bundle_tree.inventory
182
210
        self._validate_inventory(inv, revision_id)
183
 
        self._validate_revision(inv, revision_id)
 
211
        self._validate_revision(bundle_tree, revision_id)
184
212
 
185
213
        return bundle_tree
186
214
 
213
241
        for rev_info in self.revisions:
214
242
            checked[rev_info.revision_id] = True
215
243
            add_sha(rev_to_sha, rev_info.revision_id, rev_info.sha1)
216
 
                
 
244
 
217
245
        for (rev, rev_info) in zip(self.real_revisions, self.revisions):
218
246
            add_sha(inv_to_sha, rev_info.revision_id, rev_info.inventory_sha1)
219
247
 
221
249
        missing = {}
222
250
        for revision_id, sha1 in rev_to_sha.iteritems():
223
251
            if repository.has_revision(revision_id):
224
 
                testament = StrictTestament.from_revision(repository, 
 
252
                testament = StrictTestament.from_revision(repository,
225
253
                                                          revision_id)
226
 
                local_sha1 = testament.as_sha1()
 
254
                local_sha1 = self._testament_sha1_from_revision(repository,
 
255
                                                                revision_id)
227
256
                if sha1 != local_sha1:
228
 
                    raise BzrError('sha1 mismatch. For revision id {%s}' 
 
257
                    raise BzrError('sha1 mismatch. For revision id {%s}'
229
258
                            'local: %s, bundle: %s' % (revision_id, local_sha1, sha1))
230
259
                else:
231
260
                    count += 1
232
261
            elif revision_id not in checked:
233
262
                missing[revision_id] = sha1
234
263
 
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
 
 
250
264
        if len(missing) > 0:
251
265
            # I don't know if this is an error yet
252
266
            warning('Not all revision hashes could be validated.'
253
267
                    ' Unable validate %d hashes' % len(missing))
254
268
        mutter('Verified %d sha hashes for the bundle.' % count)
 
269
        self._validated_revisions_against_repo = True
255
270
 
256
271
    def _validate_inventory(self, inv, revision_id):
257
272
        """At this point we should have generated the BundleTree,
258
273
        so build up an inventory, and make sure the hashes match.
259
274
        """
260
 
 
261
 
        assert inv is not None
262
 
 
263
275
        # Now we should have a complete inventory entry.
264
276
        s = serializer_v5.write_inventory_to_string(inv)
265
277
        sha1 = sha_string(s)
266
278
        # Target revision is the last entry in the real_revisions list
267
279
        rev = self.get_revision(revision_id)
268
 
        assert rev.revision_id == revision_id
 
280
        if rev.revision_id != revision_id:
 
281
            raise AssertionError()
269
282
        if sha1 != rev.inventory_sha1:
270
 
            open(',,bogus-inv', 'wb').write(s)
 
283
            f = open(',,bogus-inv', 'wb')
 
284
            try:
 
285
                f.write(s)
 
286
            finally:
 
287
                f.close()
271
288
            warning('Inventory sha hash mismatch for revision %s. %s'
272
289
                    ' != %s' % (revision_id, sha1, rev.inventory_sha1))
273
290
 
274
 
    def _validate_revision(self, inventory, revision_id):
 
291
    def _validate_revision(self, tree, revision_id):
275
292
        """Make sure all revision entries match their checksum."""
276
293
 
277
 
        # This is a mapping from each revision id to it's sha hash
 
294
        # This is a mapping from each revision id to its sha hash
278
295
        rev_to_sha1 = {}
279
 
        
 
296
 
280
297
        rev = self.get_revision(revision_id)
281
298
        rev_info = self.get_revision_info(revision_id)
282
 
        assert rev.revision_id == rev_info.revision_id
283
 
        assert rev.revision_id == revision_id
284
 
        sha1 = StrictTestament(rev, inventory).as_sha1()
 
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)
285
304
        if sha1 != rev_info.sha1:
286
305
            raise TestamentMismatch(rev.revision_id, rev_info.sha1, sha1)
287
306
        if rev.revision_id in rev_to_sha1:
298
317
 
299
318
        def get_rev_id(last_changed, path, kind):
300
319
            if last_changed is not None:
301
 
                changed_revision_id = last_changed.decode('utf-8')
 
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)
302
324
            else:
303
325
                changed_revision_id = revision_id
304
326
            bundle_tree.note_last_changed(path, changed_revision_id)
311
333
                try:
312
334
                    name, value = info_item.split(':', 1)
313
335
                except ValueError:
314
 
                    raise 'Value %r has no colon' % info_item
 
336
                    raise ValueError('Value %r has no colon' % info_item)
315
337
                if name == 'last-changed':
316
338
                    last_changed = value
317
339
                elif name == 'executable':
318
 
                    assert value in ('yes', 'no'), value
319
340
                    val = (value == 'yes')
320
341
                    bundle_tree.note_executable(new_path, val)
321
342
                elif name == 'target':
325
346
            return last_changed, encoding
326
347
 
327
348
        def do_patch(path, lines, encoding):
328
 
            if encoding is not None:
329
 
                assert encoding == 'base64'
 
349
            if encoding == 'base64':
330
350
                patch = base64.decodestring(''.join(lines))
331
 
            else:
 
351
            elif encoding is None:
332
352
                patch =  ''.join(lines)
 
353
            else:
 
354
                raise ValueError(encoding)
333
355
            bundle_tree.note_patch(path, patch)
334
356
 
335
357
        def renamed(kind, extra, lines):
371
393
            if not info[1].startswith('file-id:'):
372
394
                raise BzrError('The file-id should follow the path for an add'
373
395
                        ': %r' % extra)
374
 
            file_id = info[1][8:]
 
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)
375
399
 
376
400
            bundle_tree.note_id(file_id, path, kind)
377
401
            # this will be overridden in extra_info if executable is specified.
393
417
            revision = get_rev_id(last_modified, path, kind)
394
418
            if lines:
395
419
                do_patch(path, lines, encoding)
396
 
            
 
420
 
397
421
        valid_actions = {
398
422
            'renamed':renamed,
399
423
            'removed':removed,
422
446
                        ' (unrecognized action): %r' % action_line)
423
447
            valid_actions[action](kind, extra, lines)
424
448
 
 
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
 
425
465
 
426
466
class BundleTree(Tree):
 
467
 
427
468
    def __init__(self, base_tree, revision_id):
428
469
        self.base_tree = base_tree
429
470
        self._renamed = {} # Mapping from old_path => new_path
445
486
 
446
487
    def note_rename(self, old_path, new_path):
447
488
        """A file/directory has been renamed from old_path => new_path"""
448
 
        assert new_path not in self._renamed
449
 
        assert old_path not in self._renamed_r
 
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)
450
493
        self._renamed[new_path] = old_path
451
494
        self._renamed_r[old_path] = new_path
452
495
 
482
525
 
483
526
    def old_path(self, new_path):
484
527
        """Get the old_path (path in the base_tree) for the file at new_path"""
485
 
        assert new_path[:1] not in ('\\', '/')
 
528
        if new_path[:1] in ('\\', '/'):
 
529
            raise ValueError(new_path)
486
530
        old_path = self._renamed.get(new_path)
487
531
        if old_path is not None:
488
532
            return old_path
502
546
        #renamed_r
503
547
        if old_path in self._renamed_r:
504
548
            return None
505
 
        return old_path 
 
549
        return old_path
506
550
 
507
551
    def new_path(self, old_path):
508
552
        """Get the new_path (path in the target_tree) for the file at old_path
509
553
        in the base tree.
510
554
        """
511
 
        assert old_path[:1] not in ('\\', '/')
 
555
        if old_path[:1] in ('\\', '/'):
 
556
            raise ValueError(old_path)
512
557
        new_path = self._renamed_r.get(old_path)
513
558
        if new_path is not None:
514
559
            return new_path
527
572
        #renamed_r
528
573
        if new_path in self._renamed:
529
574
            return None
530
 
        return new_path 
 
575
        return new_path
531
576
 
532
577
    def path2id(self, path):
533
578
        """Return the id of the file present at path in the target tree."""
539
584
            return None
540
585
        if old_path in self.deleted:
541
586
            return None
542
 
        if hasattr(self.base_tree, 'path2id'):
 
587
        if getattr(self.base_tree, 'path2id', None) is not None:
543
588
            return self.base_tree.path2id(old_path)
544
589
        else:
545
590
            return self.base_tree.inventory.path2id(old_path)
567
612
                return None
568
613
        new_path = self.id2path(file_id)
569
614
        return self.base_tree.path2id(new_path)
570
 
        
 
615
 
571
616
    def get_file(self, file_id):
572
617
        """Return a file-like object containing the new contents of the
573
618
        file given by file_id.
577
622
                then be cached.
578
623
        """
579
624
        base_id = self.old_contents_id(file_id)
580
 
        if base_id is not None:
 
625
        if (base_id is not None and
 
626
            base_id != self.base_tree.inventory.root.file_id):
581
627
            patch_original = self.base_tree.get_file(base_id)
582
628
        else:
583
629
            patch_original = None
584
630
        file_patch = self.patches.get(self.id2path(file_id))
585
631
        if file_patch is None:
586
 
            if (patch_original is None and 
 
632
            if (patch_original is None and
587
633
                self.get_kind(file_id) == 'directory'):
588
634
                return StringIO()
589
 
            assert patch_original is not None, "None: %s" % file_id
 
635
            if patch_original is None:
 
636
                raise AssertionError("None: %s" % file_id)
590
637
            return patch_original
591
638
 
592
 
        assert not file_patch.startswith('\\'), \
593
 
            'Malformed patch for %s, %r' % (file_id, file_patch)
 
639
        if file_patch.startswith('\\'):
 
640
            raise ValueError(
 
641
                'Malformed patch for %s, %r' % (file_id, file_patch))
594
642
        return patched_file(file_patch, patch_original)
595
643
 
596
644
    def get_symlink_target(self, file_id):
616
664
        path = self.id2path(file_id)
617
665
        if path in self._last_changed:
618
666
            return self._last_changed[path]
619
 
        return self.base_tree.inventory[file_id].revision
 
667
        return self.base_tree.get_file_revision(file_id)
620
668
 
621
669
    def get_size_and_sha1(self, file_id):
622
670
        """Return the size and sha1 hash of the given file id.
643
691
        This need to be called before ever accessing self.inventory
644
692
        """
645
693
        from os.path import dirname, basename
646
 
 
647
 
        assert self.base_tree is not None
648
694
        base_inv = self.base_tree.inventory
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)
 
695
        inv = Inventory(None, self.revision_id)
656
696
 
657
697
        def add_entry(file_id):
658
698
            path = self.id2path(file_id)
659
699
            if path is None:
660
700
                return
661
 
            parent_path = dirname(path)
662
 
            if parent_path == u'':
663
 
                parent_id = root_id
 
701
            if path == '':
 
702
                parent_id = None
664
703
            else:
 
704
                parent_path = dirname(path)
665
705
                parent_id = self.path2id(parent_path)
666
706
 
667
707
            kind = self.get_kind(file_id)
678
718
                ie.symlink_target = self.get_symlink_target(file_id)
679
719
            ie.revision = revision_id
680
720
 
681
 
            if kind in ('directory', 'symlink'):
682
 
                ie.text_size, ie.text_sha1 = None, None
683
 
            else:
 
721
            if kind == 'file':
684
722
                ie.text_size, ie.text_sha1 = self.get_size_and_sha1(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)
 
723
                if ie.text_size is None:
 
724
                    raise BzrError(
 
725
                        'Got a text_size of None for file_id %r' % file_id)
687
726
            inv.add(ie)
688
727
 
689
728
        sorted_entries = self.sorted_path_id()
690
729
        for path, file_id in sorted_entries:
691
 
            if file_id == inv.root.file_id:
692
 
                continue
693
730
            add_entry(file_id)
694
731
 
695
732
        return inv
705
742
        for path, entry in self.inventory.iter_entries():
706
743
            yield entry.file_id
707
744
 
 
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
 
708
762
    def sorted_path_id(self):
709
763
        paths = []
710
764
        for result in self._new_id.iteritems():