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
22
from cStringIO import StringIO
29
from . import apply_bundle
30
from ...errors import (
30
from brzlib.bundle import apply_bundle
31
from brzlib.errors import (
35
from ..inventory import (
35
from brzlib.inventory import (
37
37
InventoryDirectory,
41
from ...osutils import sha_string, sha_strings, pathjoin
42
from ...revision import Revision, NULL_REVISION
43
from ..testament import StrictTestament
44
from ...trace import mutter, warning
49
from ..xml5 import serializer_v5
41
from brzlib.osutils import sha_string, pathjoin
42
from brzlib.revision import Revision, NULL_REVISION
43
from brzlib.testament import StrictTestament
44
from brzlib.trace import mutter, warning
45
from brzlib.tree import Tree
46
from brzlib.xml5 import serializer_v5
52
49
class RevisionInfo(object):
53
50
"""Gets filled out for each revision object that is read.
56
52
def __init__(self, revision_id):
57
53
self.revision_id = revision_id
74
70
def as_revision(self):
75
71
rev = Revision(revision_id=self.revision_id,
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))
72
committer=self.committer,
73
timestamp=float(self.timestamp),
74
timezone=int(self.timezone),
75
inventory_sha1=self.inventory_sha1,
76
message='\n'.join(self.message))
82
78
if self.parent_ids:
83
79
rev.parent_ids.extend(self.parent_ids)
281
275
so build up an inventory, and make sure the hashes match.
283
277
# Now we should have a complete inventory entry.
284
cs = serializer_v5.write_inventory_to_chunks(inv)
285
sha1 = sha_strings(cs)
278
s = serializer_v5.write_inventory_to_string(inv)
286
280
# Target revision is the last entry in the real_revisions list
287
281
rev = self.get_revision(revision_id)
288
282
if rev.revision_id != revision_id:
289
283
raise AssertionError()
290
284
if sha1 != rev.inventory_sha1:
291
with open(',,bogus-inv', 'wb') as f:
285
f = open(',,bogus-inv', 'wb')
293
290
warning('Inventory sha hash mismatch for revision %s. %s'
294
291
' != %s' % (revision_id, sha1, rev.inventory_sha1))
296
def _testament(self, revision, tree):
297
raise NotImplementedError(self._testament)
299
293
def _validate_revision(self, tree, revision_id):
300
294
"""Make sure all revision entries match their checksum."""
393
387
info = extra.split(' // ')
394
388
if len(info) <= 1:
395
389
raise BzrError('add action lines require the path and file id'
397
391
elif len(info) > 5:
398
392
raise BzrError('add action lines have fewer than 5 entries.'
401
395
if not info[1].startswith('file-id:'):
402
396
raise BzrError('The file-id should follow the path for an add'
404
398
# This will be Unicode because of how the stream is read. Turn it
405
399
# back into a utf8 file_id
406
file_id = cache_utf8.encode(info[1][8:])
400
file_id = osutils.safe_file_id(info[1][8:], warn=False)
408
402
bundle_tree.note_id(file_id, path, kind)
409
403
# this will be overridden in extra_info if executable is specified.
427
421
do_patch(path, lines, encoding)
429
423
valid_actions = {
435
429
for action_line, lines in \
436
self.get_revision_info(revision_id).tree_actions:
430
self.get_revision_info(revision_id).tree_actions:
437
431
first = action_line.find(' ')
439
433
raise BzrError('Bogus action line'
440
' (no opening space): %r' % action_line)
441
second = action_line.find(' ', first + 1)
434
' (no opening space): %r' % action_line)
435
second = action_line.find(' ', first+1)
443
437
raise BzrError('Bogus action line'
444
' (missing second space): %r' % action_line)
438
' (missing second space): %r' % action_line)
445
439
action = action_line[:first]
446
kind = action_line[first + 1:second]
440
kind = action_line[first+1:second]
447
441
if kind not in ('file', 'directory', 'symlink'):
448
442
raise BzrError('Bogus action line'
449
' (invalid object kind %r): %r' % (kind, action_line))
450
extra = action_line[second + 1:]
443
' (invalid object kind %r): %r' % (kind, action_line))
444
extra = action_line[second+1:]
452
446
if action not in valid_actions:
453
447
raise BzrError('Bogus action line'
454
' (unrecognized action): %r' % action_line)
448
' (unrecognized action): %r' % action_line)
455
449
valid_actions[action](kind, extra, lines)
457
451
def install_revisions(self, target_repo, stream_input=True):
476
470
def __init__(self, base_tree, revision_id):
477
471
self.base_tree = base_tree
478
self._renamed = {} # Mapping from old_path => new_path
479
self._renamed_r = {} # new_path => old_path
480
self._new_id = {} # new_path => new_id
481
self._new_id_r = {} # new_id => new_path
482
self._kinds = {} # new_path => kind
483
self._last_changed = {} # new_id => revision_id
484
self._executable = {} # new_id => executable value
472
self._renamed = {} # Mapping from old_path => new_path
473
self._renamed_r = {} # new_path => old_path
474
self._new_id = {} # new_path => new_id
475
self._new_id_r = {} # new_id => new_path
476
self._kinds = {} # new_id => kind
477
self._last_changed = {} # new_id => revision_id
478
self._executable = {} # new_id => executable value
485
479
self.patches = {}
486
self._targets = {} # new path => new symlink target
480
self._targets = {} # new path => new symlink target
487
481
self.deleted = []
482
self.contents_by_id = True
488
483
self.revision_id = revision_id
489
484
self._inventory = None
490
self._base_inter = InterTree.get(self.base_tree, self)
492
486
def __str__(self):
493
487
return pprint.pformat(self.__dict__)
505
499
"""Files that don't exist in base need a new id."""
506
500
self._new_id[new_path] = new_id
507
501
self._new_id_r[new_id] = new_path
508
self._kinds[new_path] = kind
502
self._kinds[new_id] = kind
510
504
def note_last_changed(self, file_id, revision_id):
511
505
if (file_id in self._last_changed
512
506
and self._last_changed[file_id] != revision_id):
513
507
raise BzrError('Mismatched last-changed revision for file_id {%s}'
514
': %s != %s' % (file_id,
515
self._last_changed[file_id],
508
': %s != %s' % (file_id,
509
self._last_changed[file_id],
517
511
self._last_changed[file_id] = revision_id
519
513
def note_patch(self, new_path, patch):
595
592
return self.base_tree.path2id(old_path)
597
def id2path(self, file_id, recurse='down'):
594
def id2path(self, file_id):
598
595
"""Return the new path in the target tree of the file with id file_id"""
599
596
path = self._new_id_r.get(file_id)
600
597
if path is not None:
602
old_path = self.base_tree.id2path(file_id, recurse)
599
old_path = self.base_tree.id2path(file_id)
603
600
if old_path is None:
604
raise NoSuchId(file_id, self)
605
602
if old_path in self.deleted:
606
raise NoSuchId(file_id, self)
607
new_path = self.new_path(old_path)
609
raise NoSuchId(file_id, self)
612
def get_file(self, path):
604
return self.new_path(old_path)
606
def old_contents_id(self, file_id):
607
"""Return the id in the base_tree for the given file_id.
608
Return None if the file did not exist in base.
610
if self.contents_by_id:
611
if self.base_tree.has_id(file_id):
615
new_path = self.id2path(file_id)
616
return self.base_tree.path2id(new_path)
618
def get_file(self, file_id):
613
619
"""Return a file-like object containing the new contents of the
614
620
file given by file_id.
617
623
in the text-store, so that the file contents would
620
old_path = self._base_inter.find_source_path(path)
626
base_id = self.old_contents_id(file_id)
627
if (base_id is not None and
628
base_id != self.base_tree.get_root_id()):
629
patch_original = self.base_tree.get_file(base_id)
622
631
patch_original = None
624
patch_original = self.base_tree.get_file(old_path)
625
file_patch = self.patches.get(path)
632
file_patch = self.patches.get(self.id2path(file_id))
626
633
if file_patch is None:
627
634
if (patch_original is None and
628
self.kind(path) == 'directory'):
635
self.kind(file_id) == 'directory'):
630
637
if patch_original is None:
631
638
raise AssertionError("None: %s" % file_id)
632
639
return patch_original
634
if file_patch.startswith(b'\\'):
641
if file_patch.startswith('\\'):
635
642
raise ValueError(
636
643
'Malformed patch for %s, %r' % (file_id, file_patch))
637
644
return patched_file(file_patch, patch_original)
639
def get_symlink_target(self, path):
646
def get_symlink_target(self, file_id, path=None):
648
path = self.id2path(file_id)
641
650
return self._targets[path]
643
old_path = self.old_path(path)
644
return self.base_tree.get_symlink_target(old_path)
646
def kind(self, path):
648
return self._kinds[path]
650
old_path = self.old_path(path)
651
return self.base_tree.kind(old_path)
653
def get_file_revision(self, path):
652
return self.base_tree.get_symlink_target(file_id)
654
def kind(self, file_id):
655
if file_id in self._kinds:
656
return self._kinds[file_id]
657
return self.base_tree.kind(file_id)
659
def get_file_revision(self, file_id):
660
path = self.id2path(file_id)
654
661
if path in self._last_changed:
655
662
return self._last_changed[path]
657
old_path = self.old_path(path)
658
return self.base_tree.get_file_revision(old_path)
664
return self.base_tree.get_file_revision(file_id)
660
def is_executable(self, path):
666
def is_executable(self, file_id):
667
path = self.id2path(file_id)
661
668
if path in self._executable:
662
669
return self._executable[path]
664
old_path = self.old_path(path)
665
return self.base_tree.is_executable(old_path)
671
return self.base_tree.is_executable(file_id)
667
def get_last_changed(self, path):
673
def get_last_changed(self, file_id):
674
path = self.id2path(file_id)
668
675
if path in self._last_changed:
669
676
return self._last_changed[path]
670
old_path = self.old_path(path)
671
return self.base_tree.get_file_revision(old_path)
677
return self.base_tree.get_file_revision(file_id)
673
def get_size_and_sha1(self, new_path):
679
def get_size_and_sha1(self, file_id):
674
680
"""Return the size and sha1 hash of the given file id.
675
681
If the file was not locally modified, this is extracted
676
682
from the base_tree. Rather than re-reading the file.
684
new_path = self.id2path(file_id)
678
685
if new_path is None:
679
686
return None, None
680
687
if new_path not in self.patches:
681
688
# If the entry does not have a patch, then the
682
689
# contents must be the same as in the base_tree
683
base_path = self.old_path(new_path)
684
text_size = self.base_tree.get_file_size(base_path)
685
text_sha1 = self.base_tree.get_file_sha1(base_path)
690
text_size = self.base_tree.get_file_size(file_id)
691
text_sha1 = self.base_tree.get_file_sha1(file_id)
686
692
return text_size, text_sha1
687
fileobj = self.get_file(new_path)
693
fileobj = self.get_file(file_id)
688
694
content = fileobj.read()
689
695
return len(content), sha_string(content)
696
702
from os.path import dirname, basename
697
703
inv = Inventory(None, self.revision_id)
699
def add_entry(path, file_id):
705
def add_entry(file_id):
706
path = self.id2path(file_id)
703
712
parent_path = dirname(path)
704
713
parent_id = self.path2id(parent_path)
706
kind = self.kind(path)
707
revision_id = self.get_last_changed(path)
715
kind = self.kind(file_id)
716
revision_id = self.get_last_changed(file_id)
709
718
name = basename(path)
710
719
if kind == 'directory':
711
720
ie = InventoryDirectory(file_id, name, parent_id)
712
721
elif kind == 'file':
713
722
ie = InventoryFile(file_id, name, parent_id)
714
ie.executable = self.is_executable(path)
723
ie.executable = self.is_executable(file_id)
715
724
elif kind == 'symlink':
716
725
ie = InventoryLink(file_id, name, parent_id)
717
ie.symlink_target = self.get_symlink_target(path)
726
ie.symlink_target = self.get_symlink_target(file_id, path)
718
727
ie.revision = revision_id
720
729
if kind == 'file':
721
ie.text_size, ie.text_sha1 = self.get_size_and_sha1(path)
730
ie.text_size, ie.text_sha1 = self.get_size_and_sha1(file_id)
722
731
if ie.text_size is None:
724
733
'Got a text_size of None for file_id %r' % file_id)
758
765
entries = inv.iter_entries(from_dir=from_dir_id, recursive=recursive)
759
766
if inv.root is not None and not include_root and from_dir is None:
760
# skip the root for compatibility with the current apis.
767
# skip the root for compatability with the current apis.
762
769
for path, entry in entries:
763
yield path, 'V', entry.kind, entry
770
yield path, 'V', entry.kind, entry.file_id, entry
765
772
def sorted_path_id(self):
767
for result in self._new_id.items():
774
for result in self._new_id.iteritems():
768
775
paths.append(result)
769
776
for id in self.base_tree.all_file_ids():
771
path = self.id2path(id, recurse='none')
777
path = self.id2path(id)
774
780
paths.append((path, id))