17
17
"""Read in a bundle stream, and process it into a BundleReader object."""
19
from __future__ import absolute_import
22
from io import BytesIO
20
from cStringIO import StringIO
31
from . import apply_bundle
32
from ...errors import (
37
from ..inventory import (
43
from ..inventorytree import InventoryTree
44
from ...osutils import sha_string, sha_strings, pathjoin
45
from ...revision import Revision, NULL_REVISION
46
from ...sixish import (
49
from ..testament import StrictTestament
50
from ...trace import mutter, warning
55
from ..xml5 import serializer_v5
29
from bzrlib.bundle import apply_bundle
30
from bzrlib.errors import (TestamentMismatch, BzrError,
31
MalformedHeader, MalformedPatches, NotABundle)
32
from bzrlib.inventory import (Inventory, InventoryEntry,
33
InventoryDirectory, InventoryFile,
35
from bzrlib.osutils import sha_file, sha_string, pathjoin
36
from bzrlib.revision import Revision, NULL_REVISION
37
from bzrlib.testament import StrictTestament
38
from bzrlib.trace import mutter, warning
39
import bzrlib.transport
40
from bzrlib.tree import Tree
41
import bzrlib.urlutils
42
from bzrlib.xml5 import serializer_v5
58
45
class RevisionInfo(object):
59
46
"""Gets filled out for each revision object that is read.
62
48
def __init__(self, revision_id):
63
49
self.revision_id = revision_id
80
66
def as_revision(self):
81
67
rev = Revision(revision_id=self.revision_id,
82
committer=self.committer,
83
timestamp=float(self.timestamp),
84
timezone=int(self.timezone),
85
inventory_sha1=self.inventory_sha1,
86
message='\n'.join(self.message))
68
committer=self.committer,
69
timestamp=float(self.timestamp),
70
timezone=int(self.timezone),
71
inventory_sha1=self.inventory_sha1,
72
message='\n'.join(self.message))
88
74
if self.parent_ids:
89
75
rev.parent_ids.extend(self.parent_ids)
287
271
so build up an inventory, and make sure the hashes match.
289
273
# Now we should have a complete inventory entry.
290
cs = serializer_v5.write_inventory_to_chunks(inv)
291
sha1 = sha_strings(cs)
274
s = serializer_v5.write_inventory_to_string(inv)
292
276
# Target revision is the last entry in the real_revisions list
293
277
rev = self.get_revision(revision_id)
294
278
if rev.revision_id != revision_id:
295
279
raise AssertionError()
296
280
if sha1 != rev.inventory_sha1:
297
with open(',,bogus-inv', 'wb') as f:
281
open(',,bogus-inv', 'wb').write(s)
299
282
warning('Inventory sha hash mismatch for revision %s. %s'
300
283
' != %s' % (revision_id, sha1, rev.inventory_sha1))
302
def _testament(self, revision, tree):
303
raise NotImplementedError(self._testament)
305
def _validate_revision(self, tree, revision_id):
285
def _validate_revision(self, inventory, revision_id):
306
286
"""Make sure all revision entries match their checksum."""
308
# This is a mapping from each revision id to its sha hash
288
# This is a mapping from each revision id to it's sha hash
311
291
rev = self.get_revision(revision_id)
399
379
info = extra.split(' // ')
400
380
if len(info) <= 1:
401
381
raise BzrError('add action lines require the path and file id'
403
383
elif len(info) > 5:
404
384
raise BzrError('add action lines have fewer than 5 entries.'
407
387
if not info[1].startswith('file-id:'):
408
388
raise BzrError('The file-id should follow the path for an add'
410
390
# This will be Unicode because of how the stream is read. Turn it
411
391
# back into a utf8 file_id
412
file_id = cache_utf8.encode(info[1][8:])
392
file_id = osutils.safe_file_id(info[1][8:], warn=False)
414
394
bundle_tree.note_id(file_id, path, kind)
415
395
# this will be overridden in extra_info if executable is specified.
433
413
do_patch(path, lines, encoding)
435
415
valid_actions = {
441
421
for action_line, lines in \
442
self.get_revision_info(revision_id).tree_actions:
422
self.get_revision_info(revision_id).tree_actions:
443
423
first = action_line.find(' ')
445
425
raise BzrError('Bogus action line'
446
' (no opening space): %r' % action_line)
447
second = action_line.find(' ', first + 1)
426
' (no opening space): %r' % action_line)
427
second = action_line.find(' ', first+1)
449
429
raise BzrError('Bogus action line'
450
' (missing second space): %r' % action_line)
430
' (missing second space): %r' % action_line)
451
431
action = action_line[:first]
452
kind = action_line[first + 1:second]
432
kind = action_line[first+1:second]
453
433
if kind not in ('file', 'directory', 'symlink'):
454
434
raise BzrError('Bogus action line'
455
' (invalid object kind %r): %r' % (kind, action_line))
456
extra = action_line[second + 1:]
435
' (invalid object kind %r): %r' % (kind, action_line))
436
extra = action_line[second+1:]
458
438
if action not in valid_actions:
459
439
raise BzrError('Bogus action line'
460
' (unrecognized action): %r' % action_line)
440
' (unrecognized action): %r' % action_line)
461
441
valid_actions[action](kind, extra, lines)
463
443
def install_revisions(self, target_repo, stream_input=True):
477
457
return None, self.target, 'inapplicable'
480
class BundleTree(InventoryTree):
460
class BundleTree(Tree):
482
461
def __init__(self, base_tree, revision_id):
483
462
self.base_tree = base_tree
484
self._renamed = {} # Mapping from old_path => new_path
485
self._renamed_r = {} # new_path => old_path
486
self._new_id = {} # new_path => new_id
487
self._new_id_r = {} # new_id => new_path
488
self._kinds = {} # new_path => kind
489
self._last_changed = {} # new_id => revision_id
490
self._executable = {} # new_id => executable value
463
self._renamed = {} # Mapping from old_path => new_path
464
self._renamed_r = {} # new_path => old_path
465
self._new_id = {} # new_path => new_id
466
self._new_id_r = {} # new_id => new_path
467
self._kinds = {} # new_id => kind
468
self._last_changed = {} # new_id => revision_id
469
self._executable = {} # new_id => executable value
491
470
self.patches = {}
492
self._targets = {} # new path => new symlink target
471
self._targets = {} # new path => new symlink target
493
472
self.deleted = []
473
self.contents_by_id = True
494
474
self.revision_id = revision_id
495
475
self._inventory = None
496
self._base_inter = InterTree.get(self.base_tree, self)
498
477
def __str__(self):
499
478
return pprint.pformat(self.__dict__)
511
490
"""Files that don't exist in base need a new id."""
512
491
self._new_id[new_path] = new_id
513
492
self._new_id_r[new_id] = new_path
514
self._kinds[new_path] = kind
493
self._kinds[new_id] = kind
516
495
def note_last_changed(self, file_id, revision_id):
517
496
if (file_id in self._last_changed
518
497
and self._last_changed[file_id] != revision_id):
519
498
raise BzrError('Mismatched last-changed revision for file_id {%s}'
520
': %s != %s' % (file_id,
521
self._last_changed[file_id],
499
': %s != %s' % (file_id,
500
self._last_changed[file_id],
523
502
self._last_changed[file_id] = revision_id
525
504
def note_patch(self, new_path, patch):
599
578
if old_path in self.deleted:
601
return self.base_tree.path2id(old_path)
580
if getattr(self.base_tree, 'path2id', None) is not None:
581
return self.base_tree.path2id(old_path)
583
return self.base_tree.inventory.path2id(old_path)
603
def id2path(self, file_id, recurse='down'):
585
def id2path(self, file_id):
604
586
"""Return the new path in the target tree of the file with id file_id"""
605
587
path = self._new_id_r.get(file_id)
606
588
if path is not None:
608
old_path = self.base_tree.id2path(file_id, recurse)
590
old_path = self.base_tree.id2path(file_id)
609
591
if old_path is None:
610
raise NoSuchId(file_id, self)
611
593
if old_path in self.deleted:
612
raise NoSuchId(file_id, self)
613
new_path = self.new_path(old_path)
615
raise NoSuchId(file_id, self)
618
def get_file(self, path):
595
return self.new_path(old_path)
597
def old_contents_id(self, file_id):
598
"""Return the id in the base_tree for the given file_id.
599
Return None if the file did not exist in base.
601
if self.contents_by_id:
602
if self.base_tree.has_id(file_id):
606
new_path = self.id2path(file_id)
607
return self.base_tree.path2id(new_path)
609
def get_file(self, file_id):
619
610
"""Return a file-like object containing the new contents of the
620
611
file given by file_id.
623
614
in the text-store, so that the file contents would
626
old_path = self._base_inter.find_source_path(path)
617
base_id = self.old_contents_id(file_id)
618
if (base_id is not None and
619
base_id != self.base_tree.inventory.root.file_id):
620
patch_original = self.base_tree.get_file(base_id)
628
622
patch_original = None
630
patch_original = self.base_tree.get_file(old_path)
631
file_patch = self.patches.get(path)
623
file_patch = self.patches.get(self.id2path(file_id))
632
624
if file_patch is None:
633
625
if (patch_original is None and
634
self.kind(path) == 'directory'):
626
self.get_kind(file_id) == 'directory'):
636
628
if patch_original is None:
637
629
raise AssertionError("None: %s" % file_id)
638
630
return patch_original
640
if file_patch.startswith(b'\\'):
632
if file_patch.startswith('\\'):
641
633
raise ValueError(
642
634
'Malformed patch for %s, %r' % (file_id, file_patch))
643
635
return patched_file(file_patch, patch_original)
645
def get_symlink_target(self, path):
647
return self._targets[path]
649
old_path = self.old_path(path)
650
return self.base_tree.get_symlink_target(old_path)
652
def kind(self, path):
654
return self._kinds[path]
656
old_path = self.old_path(path)
657
return self.base_tree.kind(old_path)
659
def get_file_revision(self, path):
660
if path in self._last_changed:
661
return self._last_changed[path]
663
old_path = self.old_path(path)
664
return self.base_tree.get_file_revision(old_path)
666
def is_executable(self, path):
637
def get_symlink_target(self, file_id):
638
new_path = self.id2path(file_id)
640
return self._targets[new_path]
642
return self.base_tree.get_symlink_target(file_id)
644
def get_kind(self, file_id):
645
if file_id in self._kinds:
646
return self._kinds[file_id]
647
return self.base_tree.inventory[file_id].kind
649
def is_executable(self, file_id):
650
path = self.id2path(file_id)
667
651
if path in self._executable:
668
652
return self._executable[path]
670
old_path = self.old_path(path)
671
return self.base_tree.is_executable(old_path)
654
return self.base_tree.inventory[file_id].executable
673
def get_last_changed(self, path):
656
def get_last_changed(self, file_id):
657
path = self.id2path(file_id)
674
658
if path in self._last_changed:
675
659
return self._last_changed[path]
676
old_path = self.old_path(path)
677
return self.base_tree.get_file_revision(old_path)
660
return self.base_tree.inventory[file_id].revision
679
def get_size_and_sha1(self, new_path):
662
def get_size_and_sha1(self, file_id):
680
663
"""Return the size and sha1 hash of the given file id.
681
664
If the file was not locally modified, this is extracted
682
665
from the base_tree. Rather than re-reading the file.
667
new_path = self.id2path(file_id)
684
668
if new_path is None:
685
669
return None, None
686
670
if new_path not in self.patches:
687
671
# If the entry does not have a patch, then the
688
672
# contents must be the same as in the base_tree
689
base_path = self.old_path(new_path)
690
text_size = self.base_tree.get_file_size(base_path)
691
text_sha1 = self.base_tree.get_file_sha1(base_path)
692
return text_size, text_sha1
693
fileobj = self.get_file(new_path)
673
ie = self.base_tree.inventory[file_id]
674
if ie.text_size is None:
675
return ie.text_size, ie.text_sha1
676
return int(ie.text_size), ie.text_sha1
677
fileobj = self.get_file(file_id)
694
678
content = fileobj.read()
695
679
return len(content), sha_string(content)
700
684
This need to be called before ever accessing self.inventory
702
686
from os.path import dirname, basename
687
base_inv = self.base_tree.inventory
703
688
inv = Inventory(None, self.revision_id)
705
def add_entry(path, file_id):
690
def add_entry(file_id):
691
path = self.id2path(file_id)
709
697
parent_path = dirname(path)
710
698
parent_id = self.path2id(parent_path)
712
kind = self.kind(path)
713
revision_id = self.get_last_changed(path)
700
kind = self.get_kind(file_id)
701
revision_id = self.get_last_changed(file_id)
715
703
name = basename(path)
716
704
if kind == 'directory':
717
705
ie = InventoryDirectory(file_id, name, parent_id)
718
706
elif kind == 'file':
719
707
ie = InventoryFile(file_id, name, parent_id)
720
ie.executable = self.is_executable(path)
708
ie.executable = self.is_executable(file_id)
721
709
elif kind == 'symlink':
722
710
ie = InventoryLink(file_id, name, parent_id)
723
ie.symlink_target = self.get_symlink_target(path)
711
ie.symlink_target = self.get_symlink_target(file_id)
724
712
ie.revision = revision_id
727
ie.text_size, ie.text_sha1 = self.get_size_and_sha1(path)
728
if ie.text_size is None:
730
'Got a text_size of None for file_id %r' % file_id)
714
if kind in ('directory', 'symlink'):
715
ie.text_size, ie.text_sha1 = None, None
717
ie.text_size, ie.text_sha1 = self.get_size_and_sha1(file_id)
718
if (ie.text_size is None) and (kind == 'file'):
719
raise BzrError('Got a text_size of None for file_id %r' % file_id)
733
722
sorted_entries = self.sorted_path_id()
734
723
for path, file_id in sorted_entries:
735
add_entry(path, file_id)
743
732
# at that instant
744
733
inventory = property(_get_inventory)
746
root_inventory = property(_get_inventory)
748
def all_file_ids(self):
749
return {entry.file_id for path, entry in self.inventory.iter_entries()}
751
def all_versioned_paths(self):
752
return {path for path, entry in self.inventory.iter_entries()}
754
def list_files(self, include_root=False, from_dir=None, recursive=True):
755
# The only files returned by this are those from the version
760
from_dir_id = inv.path2id(from_dir)
761
if from_dir_id is None:
762
# Directory not versioned
764
entries = inv.iter_entries(from_dir=from_dir_id, recursive=recursive)
765
if inv.root is not None and not include_root and from_dir is None:
766
# skip the root for compatibility with the current apis.
768
for path, entry in entries:
769
yield path, 'V', entry.kind, entry
736
for path, entry in self.inventory.iter_entries():
771
739
def sorted_path_id(self):
773
for result in viewitems(self._new_id):
741
for result in self._new_id.iteritems():
774
742
paths.append(result)
775
for id in self.base_tree.all_file_ids():
777
path = self.id2path(id, recurse='none')
743
for id in self.base_tree:
744
path = self.id2path(id)
780
747
paths.append((path, id))