17
17
"""Read in a bundle stream, and process it into a BundleReader object."""
19
from __future__ import absolute_import
20
from io import BytesIO
29
30
from . import apply_bundle
30
from ...errors import (
31
from ..errors import (
35
from ..inventory import (
35
from ..bzr.inventory import (
37
37
InventoryDirectory,
41
from ..inventorytree import InventoryTree
42
from ...osutils import sha_string, sha_strings, pathjoin
43
from ...revision import Revision, NULL_REVISION
41
from ..osutils import sha_string, pathjoin
42
from ..revision import Revision, NULL_REVISION
43
from ..sixish import (
44
47
from ..testament import StrictTestament
45
from ...trace import mutter, warning
50
from ..xml5 import serializer_v5
48
from ..trace import mutter, warning
49
from ..tree import Tree
50
from ..bzr.xml5 import serializer_v5
53
53
class RevisionInfo(object):
54
54
"""Gets filled out for each revision object that is read.
57
56
def __init__(self, revision_id):
58
57
self.revision_id = revision_id
75
74
def as_revision(self):
76
75
rev = Revision(revision_id=self.revision_id,
77
committer=self.committer,
78
timestamp=float(self.timestamp),
79
timezone=int(self.timezone),
80
inventory_sha1=self.inventory_sha1,
81
message='\n'.join(self.message))
76
committer=self.committer,
77
timestamp=float(self.timestamp),
78
timezone=int(self.timezone),
79
inventory_sha1=self.inventory_sha1,
80
message='\n'.join(self.message))
83
82
if self.parent_ids:
84
83
rev.parent_ids.extend(self.parent_ids)
282
279
so build up an inventory, and make sure the hashes match.
284
281
# Now we should have a complete inventory entry.
285
cs = serializer_v5.write_inventory_to_chunks(inv)
286
sha1 = sha_strings(cs)
282
s = serializer_v5.write_inventory_to_string(inv)
287
284
# Target revision is the last entry in the real_revisions list
288
285
rev = self.get_revision(revision_id)
289
286
if rev.revision_id != revision_id:
290
287
raise AssertionError()
291
288
if sha1 != rev.inventory_sha1:
292
with open(',,bogus-inv', 'wb') as f:
289
f = open(',,bogus-inv', 'wb')
294
294
warning('Inventory sha hash mismatch for revision %s. %s'
295
295
' != %s' % (revision_id, sha1, rev.inventory_sha1))
297
def _testament(self, revision, tree):
298
raise NotImplementedError(self._testament)
300
297
def _validate_revision(self, tree, revision_id):
301
298
"""Make sure all revision entries match their checksum."""
428
424
do_patch(path, lines, encoding)
430
426
valid_actions = {
436
432
for action_line, lines in \
437
self.get_revision_info(revision_id).tree_actions:
433
self.get_revision_info(revision_id).tree_actions:
438
434
first = action_line.find(' ')
440
436
raise BzrError('Bogus action line'
441
' (no opening space): %r' % action_line)
442
second = action_line.find(' ', first + 1)
437
' (no opening space): %r' % action_line)
438
second = action_line.find(' ', first+1)
444
440
raise BzrError('Bogus action line'
445
' (missing second space): %r' % action_line)
441
' (missing second space): %r' % action_line)
446
442
action = action_line[:first]
447
kind = action_line[first + 1:second]
443
kind = action_line[first+1:second]
448
444
if kind not in ('file', 'directory', 'symlink'):
449
445
raise BzrError('Bogus action line'
450
' (invalid object kind %r): %r' % (kind, action_line))
451
extra = action_line[second + 1:]
446
' (invalid object kind %r): %r' % (kind, action_line))
447
extra = action_line[second+1:]
453
449
if action not in valid_actions:
454
450
raise BzrError('Bogus action line'
455
' (unrecognized action): %r' % action_line)
451
' (unrecognized action): %r' % action_line)
456
452
valid_actions[action](kind, extra, lines)
458
454
def install_revisions(self, target_repo, stream_input=True):
472
468
return None, self.target, 'inapplicable'
475
class BundleTree(InventoryTree):
471
class BundleTree(Tree):
477
473
def __init__(self, base_tree, revision_id):
478
474
self.base_tree = base_tree
479
self._renamed = {} # Mapping from old_path => new_path
480
self._renamed_r = {} # new_path => old_path
481
self._new_id = {} # new_path => new_id
482
self._new_id_r = {} # new_id => new_path
483
self._kinds = {} # new_path => kind
484
self._last_changed = {} # new_id => revision_id
485
self._executable = {} # new_id => executable value
475
self._renamed = {} # Mapping from old_path => new_path
476
self._renamed_r = {} # new_path => old_path
477
self._new_id = {} # new_path => new_id
478
self._new_id_r = {} # new_id => new_path
479
self._kinds = {} # new_id => kind
480
self._last_changed = {} # new_id => revision_id
481
self._executable = {} # new_id => executable value
486
482
self.patches = {}
487
self._targets = {} # new path => new symlink target
483
self._targets = {} # new path => new symlink target
488
484
self.deleted = []
485
self.contents_by_id = True
489
486
self.revision_id = revision_id
490
487
self._inventory = None
491
self._base_inter = InterTree.get(self.base_tree, self)
493
489
def __str__(self):
494
490
return pprint.pformat(self.__dict__)
506
502
"""Files that don't exist in base need a new id."""
507
503
self._new_id[new_path] = new_id
508
504
self._new_id_r[new_id] = new_path
509
self._kinds[new_path] = kind
505
self._kinds[new_id] = kind
511
507
def note_last_changed(self, file_id, revision_id):
512
508
if (file_id in self._last_changed
513
509
and self._last_changed[file_id] != revision_id):
514
510
raise BzrError('Mismatched last-changed revision for file_id {%s}'
515
': %s != %s' % (file_id,
516
self._last_changed[file_id],
511
': %s != %s' % (file_id,
512
self._last_changed[file_id],
518
514
self._last_changed[file_id] = revision_id
520
516
def note_patch(self, new_path, patch):
596
595
return self.base_tree.path2id(old_path)
598
def id2path(self, file_id, recurse='down'):
597
def id2path(self, file_id):
599
598
"""Return the new path in the target tree of the file with id file_id"""
600
599
path = self._new_id_r.get(file_id)
601
600
if path is not None:
603
old_path = self.base_tree.id2path(file_id, recurse)
602
old_path = self.base_tree.id2path(file_id)
604
603
if old_path is None:
605
raise NoSuchId(file_id, self)
606
605
if old_path in self.deleted:
607
raise NoSuchId(file_id, self)
608
new_path = self.new_path(old_path)
610
raise NoSuchId(file_id, self)
613
def get_file(self, path):
607
return self.new_path(old_path)
609
def old_contents_id(self, file_id):
610
"""Return the id in the base_tree for the given file_id.
611
Return None if the file did not exist in base.
613
if self.contents_by_id:
614
if self.base_tree.has_id(file_id):
618
new_path = self.id2path(file_id)
619
return self.base_tree.path2id(new_path)
621
def get_file(self, file_id):
614
622
"""Return a file-like object containing the new contents of the
615
623
file given by file_id.
618
626
in the text-store, so that the file contents would
621
old_path = self._base_inter.find_source_path(path)
629
base_id = self.old_contents_id(file_id)
630
if (base_id is not None and
631
base_id != self.base_tree.get_root_id()):
632
patch_original = self.base_tree.get_file(base_id)
623
634
patch_original = None
625
patch_original = self.base_tree.get_file(old_path)
626
file_patch = self.patches.get(path)
635
file_patch = self.patches.get(self.id2path(file_id))
627
636
if file_patch is None:
628
637
if (patch_original is None and
629
self.kind(path) == 'directory'):
638
self.kind(file_id) == 'directory'):
631
640
if patch_original is None:
632
641
raise AssertionError("None: %s" % file_id)
633
642
return patch_original
635
if file_patch.startswith(b'\\'):
644
if file_patch.startswith('\\'):
636
645
raise ValueError(
637
646
'Malformed patch for %s, %r' % (file_id, file_patch))
638
647
return patched_file(file_patch, patch_original)
640
def get_symlink_target(self, path):
649
def get_symlink_target(self, file_id, path=None):
651
path = self.id2path(file_id)
642
653
return self._targets[path]
644
old_path = self.old_path(path)
645
return self.base_tree.get_symlink_target(old_path)
647
def kind(self, path):
649
return self._kinds[path]
651
old_path = self.old_path(path)
652
return self.base_tree.kind(old_path)
654
def get_file_revision(self, path):
655
return self.base_tree.get_symlink_target(file_id)
657
def kind(self, file_id):
658
if file_id in self._kinds:
659
return self._kinds[file_id]
660
return self.base_tree.kind(file_id)
662
def get_file_revision(self, file_id):
663
path = self.id2path(file_id)
655
664
if path in self._last_changed:
656
665
return self._last_changed[path]
658
old_path = self.old_path(path)
659
return self.base_tree.get_file_revision(old_path)
667
return self.base_tree.get_file_revision(file_id)
661
def is_executable(self, path):
669
def is_executable(self, file_id):
670
path = self.id2path(file_id)
662
671
if path in self._executable:
663
672
return self._executable[path]
665
old_path = self.old_path(path)
666
return self.base_tree.is_executable(old_path)
674
return self.base_tree.is_executable(file_id)
668
def get_last_changed(self, path):
676
def get_last_changed(self, file_id):
677
path = self.id2path(file_id)
669
678
if path in self._last_changed:
670
679
return self._last_changed[path]
671
old_path = self.old_path(path)
672
return self.base_tree.get_file_revision(old_path)
680
return self.base_tree.get_file_revision(file_id)
674
def get_size_and_sha1(self, new_path):
682
def get_size_and_sha1(self, file_id):
675
683
"""Return the size and sha1 hash of the given file id.
676
684
If the file was not locally modified, this is extracted
677
685
from the base_tree. Rather than re-reading the file.
687
new_path = self.id2path(file_id)
679
688
if new_path is None:
680
689
return None, None
681
690
if new_path not in self.patches:
682
691
# If the entry does not have a patch, then the
683
692
# contents must be the same as in the base_tree
684
base_path = self.old_path(new_path)
685
text_size = self.base_tree.get_file_size(base_path)
686
text_sha1 = self.base_tree.get_file_sha1(base_path)
693
text_size = self.base_tree.get_file_size(file_id)
694
text_sha1 = self.base_tree.get_file_sha1(file_id)
687
695
return text_size, text_sha1
688
fileobj = self.get_file(new_path)
696
fileobj = self.get_file(file_id)
689
697
content = fileobj.read()
690
698
return len(content), sha_string(content)
697
705
from os.path import dirname, basename
698
706
inv = Inventory(None, self.revision_id)
700
def add_entry(path, file_id):
708
def add_entry(file_id):
709
path = self.id2path(file_id)
704
715
parent_path = dirname(path)
705
716
parent_id = self.path2id(parent_path)
707
kind = self.kind(path)
708
revision_id = self.get_last_changed(path)
718
kind = self.kind(file_id)
719
revision_id = self.get_last_changed(file_id)
710
721
name = basename(path)
711
722
if kind == 'directory':
712
723
ie = InventoryDirectory(file_id, name, parent_id)
713
724
elif kind == 'file':
714
725
ie = InventoryFile(file_id, name, parent_id)
715
ie.executable = self.is_executable(path)
726
ie.executable = self.is_executable(file_id)
716
727
elif kind == 'symlink':
717
728
ie = InventoryLink(file_id, name, parent_id)
718
ie.symlink_target = self.get_symlink_target(path)
729
ie.symlink_target = self.get_symlink_target(file_id, path)
719
730
ie.revision = revision_id
721
732
if kind == 'file':
722
ie.text_size, ie.text_sha1 = self.get_size_and_sha1(path)
733
ie.text_size, ie.text_sha1 = self.get_size_and_sha1(file_id)
723
734
if ie.text_size is None:
725
736
'Got a text_size of None for file_id %r' % file_id)
759
767
entries = inv.iter_entries(from_dir=from_dir_id, recursive=recursive)
760
768
if inv.root is not None and not include_root and from_dir is None:
761
# skip the root for compatibility with the current apis.
769
# skip the root for compatability with the current apis.
763
771
for path, entry in entries:
764
yield path, 'V', entry.kind, entry
772
yield path, 'V', entry.kind, entry.file_id, entry
766
774
def sorted_path_id(self):
768
for result in self._new_id.items():
776
for result in viewitems(self._new_id):
769
777
paths.append(result)
770
778
for id in self.base_tree.all_file_ids():
772
path = self.id2path(id, recurse='none')
779
path = self.id2path(id)
775
782
paths.append((path, id))