23
23
# But those depend on its position within a particular inventory, and
24
24
# it would be nice not to need to hold the backpointer here.
26
from __future__ import absolute_import
28
26
# This should really be an id randomly assigned when the tree is
29
27
# created, but it's not for now.
30
ROOT_ID = b"TREE_ROOT"
32
from ..lazy_import import lazy_import
30
from bzrlib.lazy_import import lazy_import
33
31
lazy_import(globals(), """
44
from breezy.bzr import (
53
from ..sixish import (
60
from ..static_tuple import StaticTuple
48
from bzrlib.errors import (
52
from bzrlib.symbol_versioning import deprecated_in, deprecated_method
53
from bzrlib.trace import mutter
54
from bzrlib.static_tuple import StaticTuple
63
57
class InventoryEntry(object):
110
104
InventoryDirectory('2325', 'wibble', parent_id='123', revision=None)
111
105
>>> i.path2id('src/wibble')
113
109
>>> i.add(InventoryFile('2326', 'wibble.c', '2325'))
114
110
InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None, revision=None)
115
>>> i.get_entry('2326')
116
112
InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None, revision=None)
117
113
>>> for path, entry in i.iter_entries():
135
131
RENAMED = 'renamed'
136
132
MODIFIED_AND_RENAMED = 'modified and renamed'
138
__slots__ = ['file_id', 'revision', 'parent_id', 'name']
140
# Attributes that all InventoryEntry instances are expected to have, but
141
# that don't vary for all kinds of entry. (e.g. symlink_target is only
142
# relevant to InventoryLink, so there's no reason to make every
143
# InventoryFile instance allocate space to hold a value for it.)
144
# Attributes that only vary for files: executable, text_sha1, text_size,
150
# Attributes that only vary for symlinks: symlink_target
151
symlink_target = None
152
# Attributes that only vary for tree-references: reference_revision
153
reference_revision = None
156
136
def detect_changes(self, old_entry):
157
137
"""Return a (text_modified, meta_modified) from this to old_entry.
199
176
candidates[ie.revision] = ie
200
177
return candidates
179
@deprecated_method(deprecated_in((1, 6, 0)))
180
def get_tar_item(self, root, dp, now, tree):
181
"""Get a tarfile item and a file stream for its content."""
182
item = tarfile.TarInfo(osutils.pathjoin(root, dp).encode('utf8'))
183
# TODO: would be cool to actually set it to the timestamp of the
184
# revision it was last changed
186
fileobj = self._put_in_tar(item, tree)
202
189
def has_text(self):
203
190
"""Return true if the object this entry represents has textual data.
225
212
Traceback (most recent call last):
226
213
InvalidEntryName: Invalid entry name: src/hello.c
215
if '/' in name or '\\' in name:
229
216
raise errors.InvalidEntryName(name=name)
230
if not isinstance(file_id, bytes):
231
raise TypeError(file_id)
217
self.executable = False
219
self.text_sha1 = None
220
self.text_size = None
232
221
self.file_id = file_id
223
self.text_id = text_id
235
224
self.parent_id = parent_id
225
self.symlink_target = None
226
self.reference_revision = None
237
228
def kind_character(self):
238
229
"""Return a short kind indicator useful for appending to names."""
239
raise errors.BzrError('unknown kind %r' % self.kind)
230
raise BzrError('unknown kind %r' % self.kind)
241
232
known_kinds = ('file', 'directory', 'symlink')
234
def _put_in_tar(self, item, tree):
235
"""populate item for stashing in a tar, and return the content stream.
237
If no content is available, return None.
239
raise BzrError("don't know how to export {%s} of kind %r" %
240
(self.file_id, self.kind))
242
@deprecated_method(deprecated_in((1, 6, 0)))
243
def put_on_disk(self, dest, dp, tree):
244
"""Create a representation of self on disk in the prefix dest.
246
This is a template method - implement _put_on_disk in subclasses.
248
fullpath = osutils.pathjoin(dest, dp)
249
self._put_on_disk(fullpath, tree)
250
# mutter(" export {%s} kind %s to %s", self.file_id,
251
# self.kind, fullpath)
253
def _put_on_disk(self, fullpath, tree):
254
"""Put this entry onto disk at fullpath, from tree tree."""
255
raise BzrError("don't know how to export {%s} of kind %r" % (self.file_id, self.kind))
257
def sorted_children(self):
258
return sorted(self.children.items())
244
261
def versionable_kind(kind):
245
262
return (kind in ('file', 'directory', 'symlink', 'tree-reference'))
400
class RootEntry(InventoryEntry):
402
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
403
'text_id', 'parent_id', 'children', 'executable',
404
'revision', 'symlink_target', 'reference_revision']
406
def _check(self, checker, rev_id):
407
"""See InventoryEntry._check"""
409
def __init__(self, file_id):
410
self.file_id = file_id
412
self.kind = 'directory'
413
self.parent_id = None
416
symbol_versioning.warn('RootEntry is deprecated as of bzr 0.10.'
417
' Please use InventoryDirectory instead.',
418
DeprecationWarning, stacklevel=2)
420
def __eq__(self, other):
421
if not isinstance(other, RootEntry):
422
return NotImplemented
424
return (self.file_id == other.file_id) \
425
and (self.children == other.children)
384
428
class InventoryDirectory(InventoryEntry):
385
429
"""A directory in an inventory."""
387
__slots__ = ['children']
431
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
432
'text_id', 'parent_id', 'children', 'executable',
433
'revision', 'symlink_target', 'reference_revision']
391
435
def _check(self, checker, rev_id):
392
436
"""See InventoryEntry._check"""
437
if (self.text_sha1 is not None or self.text_size is not None or
438
self.text_id is not None):
439
checker._report_items.append('directory {%s} has text in revision {%s}'
440
% (self.file_id, rev_id))
393
441
# In non rich root repositories we do not expect a file graph for the
395
443
if self.name == '' and not checker.rich_roots:
398
446
# to provide a per-fileid log. The hash of every directory content is
399
447
# "da..." below (the sha1sum of '').
400
448
checker.add_pending_item(rev_id,
401
(b'texts', self.file_id, self.revision), b'text',
402
b'da39a3ee5e6b4b0d3255bfef95601890afd80709')
449
('texts', self.file_id, self.revision), 'text',
450
'da39a3ee5e6b4b0d3255bfef95601890afd80709')
405
453
other = InventoryDirectory(self.file_id, self.name, self.parent_id)
411
459
def __init__(self, file_id, name, parent_id):
412
460
super(InventoryDirectory, self).__init__(file_id, name, parent_id)
413
461
self.children = {}
415
def sorted_children(self):
416
return sorted(viewitems(self.children))
462
self.kind = 'directory'
418
464
def kind_character(self):
419
465
"""See InventoryEntry.kind_character."""
468
def _put_in_tar(self, item, tree):
469
"""See InventoryEntry._put_in_tar."""
470
item.type = tarfile.DIRTYPE
477
def _put_on_disk(self, fullpath, tree):
478
"""See InventoryEntry._put_on_disk."""
423
482
class InventoryFile(InventoryEntry):
424
483
"""A file in an inventory."""
426
__slots__ = ['text_sha1', 'text_size', 'text_id', 'executable']
430
def __init__(self, file_id, name, parent_id):
431
super(InventoryFile, self).__init__(file_id, name, parent_id)
432
self.text_sha1 = None
433
self.text_size = None
435
self.executable = False
485
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
486
'text_id', 'parent_id', 'children', 'executable',
487
'revision', 'symlink_target', 'reference_revision']
437
489
def _check(self, checker, tree_revision_id):
438
490
"""See InventoryEntry._check"""
439
491
# TODO: check size too.
440
492
checker.add_pending_item(tree_revision_id,
441
(b'texts', self.file_id, self.revision), b'text',
493
('texts', self.file_id, self.revision), 'text',
443
495
if self.text_size is None:
444
496
checker._report_items.append(
463
515
def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
464
516
output_to, reverse=False):
465
517
"""See InventoryEntry._diff."""
466
from breezy.diff import DiffText
518
from bzrlib.diff import DiffText
467
519
from_file_id = self.file_id
469
521
to_file_id = to_entry.file_id
470
to_path = to_tree.id2path(to_file_id)
472
523
to_file_id = None
474
if from_file_id is not None:
475
from_path = tree.id2path(from_file_id)
479
525
to_file_id, from_file_id = from_file_id, to_file_id
480
526
tree, to_tree = to_tree, tree
481
527
from_label, to_label = to_label, from_label
482
528
differ = DiffText(tree, to_tree, output_to, 'utf-8', '', '',
484
return differ.diff_text(from_path, to_path, from_label, to_label,
485
from_file_id, to_file_id)
530
return differ.diff_text(from_file_id, to_file_id, from_label, to_label)
487
532
def has_text(self):
488
533
"""See InventoryEntry.has_text."""
536
def __init__(self, file_id, name, parent_id):
537
super(InventoryFile, self).__init__(file_id, name, parent_id)
491
540
def kind_character(self):
492
541
"""See InventoryEntry.kind_character."""
544
def _put_in_tar(self, item, tree):
545
"""See InventoryEntry._put_in_tar."""
546
item.type = tarfile.REGTYPE
547
fileobj = tree.get_file(self.file_id)
548
item.size = self.text_size
549
if tree.is_executable(self.file_id):
555
def _put_on_disk(self, fullpath, tree):
556
"""See InventoryEntry._put_on_disk."""
557
osutils.pumpfile(tree.get_file(self.file_id), file(fullpath, 'wb'))
558
if tree.is_executable(self.file_id):
559
os.chmod(fullpath, 0755)
495
561
def _read_tree_state(self, path, work_tree):
496
562
"""See InventoryEntry._read_tree_state."""
497
self.text_sha1 = work_tree.get_file_sha1(path, self.file_id)
563
self.text_sha1 = work_tree.get_file_sha1(self.file_id, path=path)
498
564
# FIXME: 20050930 probe for the text size when getting sha1
499
565
# in _read_tree_state
500
self.executable = work_tree.is_executable(path, self.file_id)
566
self.executable = work_tree.is_executable(self.file_id, path=path)
502
568
def __repr__(self):
503
569
return ("%s(%r, %r, parent_id=%r, sha1=%r, len=%s, revision=%s)"
529
595
class InventoryLink(InventoryEntry):
530
596
"""A file in an inventory."""
532
__slots__ = ['symlink_target']
536
def __init__(self, file_id, name, parent_id):
537
super(InventoryLink, self).__init__(file_id, name, parent_id)
538
self.symlink_target = None
598
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
599
'text_id', 'parent_id', 'children', 'executable',
600
'revision', 'symlink_target', 'reference_revision']
540
602
def _check(self, checker, tree_revision_id):
541
603
"""See InventoryEntry._check"""
604
if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
605
checker._report_items.append(
606
'symlink {%s} has text in revision {%s}'
607
% (self.file_id, tree_revision_id))
542
608
if self.symlink_target is None:
543
609
checker._report_items.append(
544
610
'symlink {%s} has no target in revision {%s}'
545
611
% (self.file_id, tree_revision_id))
546
612
# Symlinks are stored as ''
547
613
checker.add_pending_item(tree_revision_id,
548
(b'texts', self.file_id, self.revision), b'text',
549
b'da39a3ee5e6b4b0d3255bfef95601890afd80709')
614
('texts', self.file_id, self.revision), 'text',
615
'da39a3ee5e6b4b0d3255bfef95601890afd80709')
552
618
other = InventoryLink(self.file_id, self.name, self.parent_id)
559
625
# FIXME: which _modified field should we use ? RBC 20051003
560
626
text_modified = (self.symlink_target != old_entry.symlink_target)
561
627
if text_modified:
562
trace.mutter(" symlink target changed")
628
mutter(" symlink target changed")
563
629
meta_modified = False
564
630
return text_modified, meta_modified
566
632
def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
567
633
output_to, reverse=False):
568
634
"""See InventoryEntry._diff."""
569
from breezy.diff import DiffSymlink
635
from bzrlib.diff import DiffSymlink
570
636
old_target = self.symlink_target
571
637
if to_entry is not None:
572
638
new_target = to_entry.symlink_target
582
648
differ = DiffSymlink(old_tree, new_tree, output_to)
583
649
return differ.diff_symlink(old_target, new_target)
651
def __init__(self, file_id, name, parent_id):
652
super(InventoryLink, self).__init__(file_id, name, parent_id)
653
self.kind = 'symlink'
585
655
def kind_character(self):
586
656
"""See InventoryEntry.kind_character."""
659
def _put_in_tar(self, item, tree):
660
"""See InventoryEntry._put_in_tar."""
661
item.type = tarfile.SYMTYPE
665
item.linkname = self.symlink_target
668
def _put_on_disk(self, fullpath, tree):
669
"""See InventoryEntry._put_on_disk."""
671
os.symlink(self.symlink_target, fullpath)
673
raise BzrError("Failed to create symlink %r -> %r, error: %s" % (fullpath, self.symlink_target, e))
589
675
def _read_tree_state(self, path, work_tree):
590
676
"""See InventoryEntry._read_tree_state."""
591
self.symlink_target = work_tree.get_symlink_target(
592
work_tree.id2path(self.file_id), self.file_id)
677
self.symlink_target = work_tree.get_symlink_target(self.file_id)
594
679
def _forget_tree_state(self):
595
680
self.symlink_target = None
650
733
inserted, other than through the Inventory API.
736
def __contains__(self, file_id):
737
"""True if this entry contains a file with given id.
739
>>> inv = Inventory()
740
>>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
741
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
747
Note that this method along with __iter__ are not encouraged for use as
748
they are less clear than specific query methods - they may be rmeoved
751
return self.has_id(file_id)
653
753
def has_filename(self, filename):
654
754
return bool(self.path2id(filename))
657
757
"""Return as a string the path to file_id.
659
759
>>> i = Inventory()
660
>>> e = i.add(InventoryDirectory(b'src-id', 'src', ROOT_ID))
661
>>> e = i.add(InventoryFile(b'foo-id', 'foo.c', parent_id='src-id'))
662
>>> print i.id2path(b'foo-id')
760
>>> e = i.add(InventoryDirectory('src-id', 'src', ROOT_ID))
761
>>> e = i.add(InventoryFile('foo-id', 'foo.c', parent_id='src-id'))
762
>>> print i.id2path('foo-id')
665
765
:raises NoSuchId: If file_id is not present in the inventory.
682
782
from_dir = self.root
683
783
yield '', self.root
684
elif isinstance(from_dir, bytes):
685
from_dir = self.get_entry(from_dir)
784
elif isinstance(from_dir, basestring):
785
from_dir = self[from_dir]
687
787
# unrolling the recursive called changed the time from
688
788
# 440ms/663ms (inline/total) to 116ms/116ms
689
children = sorted(viewitems(from_dir.children))
789
children = from_dir.children.items()
690
791
if not recursive:
691
792
for name, ie in children:
720
822
# if we finished all children, pop it off the stack
723
def _preload_cache(self):
724
"""Populate any caches, we are about to access all items.
726
The default implementation does nothing, because CommonInventory doesn't
731
def iter_entries_by_dir(self, from_dir=None, specific_file_ids=None):
825
def iter_entries_by_dir(self, from_dir=None, specific_file_ids=None,
826
yield_parents=False):
732
827
"""Iterate over the entries in a directory first order.
734
829
This returns all entries for a directory before returning
736
831
lexicographically sorted order, and is a hybrid between
737
832
depth-first and breadth-first.
834
:param yield_parents: If True, yield the parents from the root leading
835
down to specific_file_ids that have been requested. This has no
836
impact if specific_file_ids is None.
739
837
:return: This yields (path, entry) pairs
741
839
if specific_file_ids and not isinstance(specific_file_ids, set):
742
840
specific_file_ids = set(specific_file_ids)
743
841
# TODO? Perhaps this should return the from_dir so that the root is
744
842
# yielded? or maybe an option?
745
if from_dir is None and specific_file_ids is None:
746
# They are iterating from the root, and have not specified any
747
# specific entries to look at. All current callers fully consume the
748
# iterator, so we can safely assume we are accessing all entries
749
self._preload_cache()
750
843
if from_dir is None:
751
844
if self.root is None:
753
846
# Optimize a common case
754
if (specific_file_ids is not None and
847
if (not yield_parents and specific_file_ids is not None and
755
848
len(specific_file_ids) == 1):
756
849
file_id = list(specific_file_ids)[0]
757
if file_id is not None:
759
path = self.id2path(file_id)
760
except errors.NoSuchId:
763
yield path, self.get_entry(file_id)
851
yield self.id2path(file_id), self[file_id]
765
853
from_dir = self.root
766
if (specific_file_ids is None or
854
if (specific_file_ids is None or yield_parents or
767
855
self.root.file_id in specific_file_ids):
768
856
yield u'', self.root
769
elif isinstance(from_dir, (str, text_type)):
770
from_dir = self.get_entry(from_dir)
857
elif isinstance(from_dir, basestring):
858
from_dir = self[from_dir]
772
860
if specific_file_ids is not None:
773
861
# TODO: jam 20070302 This could really be done as a loop rather
793
881
cur_relpath, cur_dir = stack.pop()
796
for child_name, child_ie in sorted(viewitems(cur_dir.children)):
884
for child_name, child_ie in sorted(cur_dir.children.iteritems()):
798
886
child_relpath = cur_relpath + child_name
800
888
if (specific_file_ids is None or
801
child_ie.file_id in specific_file_ids):
889
child_ie.file_id in specific_file_ids or
890
(yield_parents and child_ie.file_id in parents)):
802
891
yield child_relpath, child_ie
804
893
if child_ie.kind == 'directory':
817
906
for file_id in deletes:
818
907
delta.append((old.id2path(file_id), None, file_id, None))
819
908
for file_id in adds:
820
delta.append((None, self.id2path(file_id), file_id, self.get_entry(file_id)))
909
delta.append((None, self.id2path(file_id), file_id, self[file_id]))
821
910
for file_id in common:
822
if old.get_entry(file_id) != self.get_entry(file_id):
911
if old[file_id] != self[file_id]:
823
912
delta.append((old.id2path(file_id), self.id2path(file_id),
824
file_id, self.get_entry(file_id)))
913
file_id, self[file_id]))
916
def _get_mutable_inventory(self):
917
"""Returns a mutable copy of the object.
919
Some inventories are immutable, yet working trees, for example, needs
920
to mutate exisiting inventories instead of creating a new one.
922
raise NotImplementedError(self._get_mutable_inventory)
827
924
def make_entry(self, kind, name, parent_id, file_id=None):
828
"""Simple thunk to breezy.bzr.inventory.make_entry."""
925
"""Simple thunk to bzrlib.inventory.make_entry."""
829
926
return make_entry(kind, name, parent_id, file_id)
831
928
def entries(self):
837
934
def descend(dir_ie, dir_path):
838
kids = sorted(viewitems(dir_ie.children))
935
kids = dir_ie.children.items()
839
937
for name, ie in kids:
840
938
child_path = osutils.pathjoin(dir_path, name)
841
939
accum.append((child_path, ie))
842
940
if ie.kind == 'directory':
843
941
descend(ie, child_path)
845
if self.root is not None:
846
descend(self.root, u'')
849
def get_entry_by_path(self, relpath):
850
"""Return an inventory entry by path.
943
descend(self.root, u'')
946
def directories(self):
947
"""Return (path, entry) pairs for all directories, including the root.
950
def descend(parent_ie, parent_path):
951
accum.append((parent_path, parent_ie))
953
kids = [(ie.name, ie) for ie in parent_ie.children.itervalues() if ie.kind == 'directory']
956
for name, child_ie in kids:
957
child_path = osutils.pathjoin(parent_path, name)
958
descend(child_ie, child_path)
959
descend(self.root, u'')
962
def path2id(self, relpath):
963
"""Walk down through directories to return entry of last component.
852
965
:param relpath: may be either a list of path components, or a single
853
966
string, in which case it is automatically split.
880
993
# or raise an error?
884
def path2id(self, relpath):
885
"""Walk down through directories to return entry of last component.
887
:param relpath: may be either a list of path components, or a single
888
string, in which case it is automatically split.
890
This returns the entry of the last component in the path,
891
which may be either a file or a directory.
893
Returns None IFF the path is not found.
895
ie = self.get_entry_by_path(relpath)
996
return parent.file_id
900
998
def filter(self, specific_fileids):
901
999
"""Get an inventory view filtered against a set of file-ids.
955
1053
>>> inv = Inventory()
956
1054
>>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
957
1055
InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
958
>>> inv.get_entry('123-123').name
1056
>>> inv['123-123'].name
961
1059
Id's may be looked up from paths:
963
1061
>>> inv.path2id('hello.c')
965
>>> inv.has_id(b'123-123')
1063
>>> '123-123' in inv
968
1066
There are iterators over the contents:
1010
1108
applied the final inventory must be internally consistent, but it
1011
1109
is ok to supply changes which, if only half-applied would have an
1012
1110
invalid result - such as supplying two changes which rename two
1013
files, 'A' and 'B' with each other : [('A', 'B', b'A-id', a_entry),
1014
('B', 'A', b'B-id', b_entry)].
1111
files, 'A' and 'B' with each other : [('A', 'B', 'A-id', a_entry),
1112
('B', 'A', 'B-id', b_entry)].
1016
1114
Each change is a tuple, of the form (old_path, new_path, file_id,
1058
1156
for old_path, file_id in sorted(((op, f) for op, np, f, e in delta
1059
1157
if op is not None), reverse=True):
1060
1158
# Preserve unaltered children of file_id for later reinsertion.
1061
file_id_children = getattr(self.get_entry(file_id), 'children', {})
1159
file_id_children = getattr(self[file_id], 'children', {})
1062
1160
if len(file_id_children):
1063
1161
children[file_id] = file_id_children
1064
1162
if self.id2path(file_id) != old_path:
1138
1240
XXX: We may not want to merge this into bzr.dev.
1140
1242
if self.root is None:
1142
return iter(viewvalues(self._byid))
1244
for _, ie in self._byid.iteritems():
1144
1247
def __len__(self):
1145
1248
"""Returns number of entries."""
1146
1249
return len(self._byid)
1148
def get_entry(self, file_id):
1251
def __getitem__(self, file_id):
1149
1252
"""Return the entry for given file_id.
1151
1254
>>> inv = Inventory()
1152
1255
>>> inv.add(InventoryFile('123123', 'hello.c', ROOT_ID))
1153
1256
InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
1154
>>> inv.get_entry('123123').name
1257
>>> inv['123123'].name
1157
if not isinstance(file_id, bytes):
1158
raise TypeError(file_id)
1160
1261
return self._byid[file_id]
1161
1262
except KeyError:
1166
1267
return self._byid[file_id].kind
1168
1269
def get_child(self, parent_id, filename):
1169
return self.get_entry(parent_id).children.get(filename)
1270
return self[parent_id].children.get(filename)
1171
1272
def _add_child(self, entry):
1172
1273
"""Add an entry to the inventory, without adding it to its parent"""
1173
1274
if entry.file_id in self._byid:
1174
raise errors.BzrError(
1175
"inventory already contains entry with id {%s}" %
1275
raise BzrError("inventory already contains entry with id {%s}" %
1177
1277
self._byid[entry.file_id] = entry
1178
children = getattr(entry, 'children', {})
1179
if children is not None:
1180
for child in viewvalues(children):
1181
self._add_child(child)
1278
for child in getattr(entry, 'children', {}).itervalues():
1279
self._add_child(child)
1184
1282
def add(self, entry):
1185
1283
"""Add entry to inventory.
1285
To add a file to a branch ready to be committed, use Branch.add,
1189
1290
if entry.file_id in self._byid:
1228
1329
ie = make_entry(kind, parts[-1], parent_id, file_id)
1229
1330
return self.add(ie)
1231
def delete(self, file_id):
1332
def __delitem__(self, file_id):
1232
1333
"""Remove entry by id.
1234
1335
>>> inv = Inventory()
1235
>>> inv.add(InventoryFile(b'123', 'foo.c', ROOT_ID))
1336
>>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
1236
1337
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
1237
>>> inv.has_id(b'123')
1239
>>> inv.delete(b'123')
1240
>>> inv.has_id(b'123')
1243
ie = self.get_entry(file_id)
1244
1345
del self._byid[file_id]
1245
1346
if ie.parent_id is not None:
1246
del self.get_entry(ie.parent_id).children[ie.name]
1347
del self[ie.parent_id].children[ie.name]
1248
1349
def __eq__(self, other):
1249
1350
"""Compare two sets by comparing their contents.
1288
1389
def _make_delta(self, old):
1289
1390
"""Make an inventory delta from two inventories."""
1290
old_getter = old.get_entry
1291
new_getter = self.get_entry
1292
old_ids = set(old.iter_all_ids())
1293
new_ids = set(self.iter_all_ids())
1391
old_getter = getattr(old, '_byid', old)
1392
new_getter = self._byid
1393
old_ids = set(old_getter)
1394
new_ids = set(new_getter)
1294
1395
adds = new_ids - old_ids
1295
1396
deletes = old_ids - new_ids
1296
1397
if not adds and not deletes:
1301
1402
for file_id in deletes:
1302
1403
delta.append((old.id2path(file_id), None, file_id, None))
1303
1404
for file_id in adds:
1304
delta.append((None, self.id2path(file_id), file_id, self.get_entry(file_id)))
1405
delta.append((None, self.id2path(file_id), file_id, self[file_id]))
1305
1406
for file_id in common:
1306
new_ie = new_getter(file_id)
1307
old_ie = old_getter(file_id)
1407
new_ie = new_getter[file_id]
1408
old_ie = old_getter[file_id]
1308
1409
# If xml_serializer returns the cached InventoryEntries (rather
1309
1410
# than always doing .copy()), inlining the 'is' check saves 2.7M
1310
1411
# calls to __eq__. Under lsprof this saves 20s => 6s.
1327
1428
ie = to_find_delete.pop()
1328
1429
to_delete.append(ie.file_id)
1329
1430
if ie.kind == 'directory':
1330
to_find_delete.extend(viewvalues(ie.children))
1431
to_find_delete.extend(ie.children.values())
1331
1432
for file_id in reversed(to_delete):
1332
ie = self.get_entry(file_id)
1333
1434
del self._byid[file_id]
1334
1435
if ie.parent_id is not None:
1335
del self.get_entry(ie.parent_id).children[ie.name]
1436
del self[ie.parent_id].children[ie.name]
1337
1438
self.root = None
1346
1447
new_name = ensure_normalized_name(new_name)
1347
1448
if not is_valid_name(new_name):
1348
raise errors.BzrError("not an acceptable filename: %r" % new_name)
1449
raise BzrError("not an acceptable filename: %r" % new_name)
1350
1451
new_parent = self._byid[new_parent_id]
1351
1452
if new_name in new_parent.children:
1352
raise errors.BzrError("%r already exists in %r" %
1353
(new_name, self.id2path(new_parent_id)))
1453
raise BzrError("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
1355
1455
new_parent_idpath = self.get_idpath(new_parent_id)
1356
1456
if file_id in new_parent_idpath:
1357
raise errors.BzrError(
1358
"cannot move directory %r into a subdirectory of itself, %r"
1457
raise BzrError("cannot move directory %r into a subdirectory of itself, %r"
1359
1458
% (self.id2path(file_id), self.id2path(new_parent_id)))
1361
1460
file_ie = self._byid[file_id]
1433
1531
if entry.parent_id is not None:
1434
1532
parent_str = entry.parent_id
1437
1535
name_str = entry.name.encode("utf8")
1438
1536
if entry.kind == 'file':
1439
1537
if entry.executable:
1443
return b"file: %s\n%s\n%s\n%s\n%s\n%d\n%s" % (
1541
return "file: %s\n%s\n%s\n%s\n%s\n%d\n%s" % (
1444
1542
entry.file_id, parent_str, name_str, entry.revision,
1445
1543
entry.text_sha1, entry.text_size, exec_str)
1446
1544
elif entry.kind == 'directory':
1447
return b"dir: %s\n%s\n%s\n%s" % (
1545
return "dir: %s\n%s\n%s\n%s" % (
1448
1546
entry.file_id, parent_str, name_str, entry.revision)
1449
1547
elif entry.kind == 'symlink':
1450
return b"symlink: %s\n%s\n%s\n%s\n%s" % (
1548
return "symlink: %s\n%s\n%s\n%s\n%s" % (
1451
1549
entry.file_id, parent_str, name_str, entry.revision,
1452
1550
entry.symlink_target.encode("utf8"))
1453
1551
elif entry.kind == 'tree-reference':
1454
return b"tree: %s\n%s\n%s\n%s\n%s" % (
1552
return "tree: %s\n%s\n%s\n%s\n%s" % (
1455
1553
entry.file_id, parent_str, name_str, entry.revision,
1456
1554
entry.reference_revision)
1519
1617
keys = [StaticTuple(f,).intern() for f in directories_to_expand]
1520
1618
directories_to_expand = set()
1521
1619
items = self.parent_id_basename_to_file_id.iteritems(keys)
1522
next_file_ids = {item[1] for item in items}
1620
next_file_ids = set([item[1] for item in items])
1523
1621
next_file_ids = next_file_ids.difference(interesting)
1524
1622
interesting.update(next_file_ids)
1525
1623
for entry in self._getitems(next_file_ids):
1526
1624
if entry.kind == 'directory':
1527
1625
directories_to_expand.add(entry.file_id)
1528
children_of_parent_id.setdefault(entry.parent_id, set()
1529
).add(entry.file_id)
1626
children_of_parent_id.setdefault(entry.parent_id, []
1627
).append(entry.file_id)
1530
1628
return interesting, children_of_parent_id
1532
1630
def filter(self, specific_fileids):
1572
def _bytes_to_utf8name_key(data):
1573
"""Get the file_id, revision_id key out of data."""
1674
def _bytes_to_utf8name_key(bytes):
1675
"""Get the file_id, revision_id key out of bytes."""
1574
1676
# We don't normally care about name, except for times when we want
1575
1677
# to filter out empty names because of non rich-root...
1576
sections = data.split(b'\n')
1577
kind, file_id = sections[0].split(b': ')
1578
return (sections[2], bytesintern(file_id), bytesintern(sections[3]))
1678
sections = bytes.split('\n')
1679
kind, file_id = sections[0].split(': ')
1680
return (sections[2], intern(file_id), intern(sections[3]))
1580
1682
def _bytes_to_entry(self, bytes):
1581
1683
"""Deserialise a serialised entry."""
1582
sections = bytes.split(b'\n')
1583
if sections[0].startswith(b"file: "):
1684
sections = bytes.split('\n')
1685
if sections[0].startswith("file: "):
1584
1686
result = InventoryFile(sections[0][6:],
1585
1687
sections[2].decode('utf8'),
1587
1689
result.text_sha1 = sections[4]
1588
1690
result.text_size = int(sections[5])
1589
result.executable = sections[6] == b"Y"
1590
elif sections[0].startswith(b"dir: "):
1691
result.executable = sections[6] == "Y"
1692
elif sections[0].startswith("dir: "):
1591
1693
result = CHKInventoryDirectory(sections[0][5:],
1592
1694
sections[2].decode('utf8'),
1593
1695
sections[1], self)
1594
elif sections[0].startswith(b"symlink: "):
1696
elif sections[0].startswith("symlink: "):
1595
1697
result = InventoryLink(sections[0][9:],
1596
1698
sections[2].decode('utf8'),
1598
1700
result.symlink_target = sections[4].decode('utf8')
1599
elif sections[0].startswith(b"tree: "):
1701
elif sections[0].startswith("tree: "):
1600
1702
result = TreeReference(sections[0][6:],
1601
1703
sections[2].decode('utf8'),
1603
1705
result.reference_revision = sections[4]
1605
1707
raise ValueError("Not a serialised entry %r" % bytes)
1606
result.file_id = bytesintern(result.file_id)
1607
result.revision = bytesintern(sections[3])
1608
if result.parent_id == b'':
1708
result.file_id = intern(result.file_id)
1709
result.revision = intern(sections[3])
1710
if result.parent_id == '':
1609
1711
result.parent_id = None
1610
1712
self._fileid_to_entry_cache[result.file_id] = result
1715
def _get_mutable_inventory(self):
1716
"""See CommonInventory._get_mutable_inventory."""
1717
entries = self.iter_entries()
1718
inv = Inventory(None, self.revision_id)
1719
for path, inv_entry in entries:
1720
inv.add(inv_entry.copy())
1613
1723
def create_by_apply_delta(self, inventory_delta, new_revision_id,
1614
1724
propagate_caches=False):
1615
1725
"""Create a new CHKInventory by applying inventory_delta to this one.
1742
1852
new_key, [None, None])[1] = new_value
1743
1853
# validate that deletes are complete.
1744
1854
for file_id in deletes:
1745
entry = self.get_entry(file_id)
1855
entry = self[file_id]
1746
1856
if entry.kind != 'directory':
1748
1858
# This loop could potentially be better by using the id_basename
1749
1859
# map to just get the child file ids.
1750
for child in viewvalues(entry.children):
1860
for child in entry.children.values():
1751
1861
if child.file_id not in altered:
1752
1862
raise errors.InconsistentDelta(self.id2path(child.file_id),
1753
1863
child.file_id, "Child not deleted or reparented when "
1790
1900
:return: A CHKInventory
1792
lines = bytes.split(b'\n')
1793
if lines[-1] != b'':
1902
lines = bytes.split('\n')
1794
1904
raise AssertionError('bytes to deserialize must end with an eol')
1796
if lines[0] != b'chkinventory:':
1906
if lines[0] != 'chkinventory:':
1797
1907
raise ValueError("not a serialised CHKInventory: %r" % bytes)
1799
allowed_keys = frozenset((b'root_id', b'revision_id',
1800
b'parent_id_basename_to_file_id',
1801
b'search_key_name', b'id_to_entry'))
1909
allowed_keys = frozenset(['root_id', 'revision_id', 'search_key_name',
1910
'parent_id_basename_to_file_id',
1802
1912
for line in lines[1:]:
1803
key, value = line.split(b': ', 1)
1913
key, value = line.split(': ', 1)
1804
1914
if key not in allowed_keys:
1805
1915
raise errors.BzrError('Unknown key in inventory: %r\n%r'
1806
1916
% (key, bytes))
1808
1918
raise errors.BzrError('Duplicate key in inventory: %r\n%r'
1809
1919
% (key, bytes))
1810
1920
info[key] = value
1811
revision_id = bytesintern(info[b'revision_id'])
1812
root_id = bytesintern(info[b'root_id'])
1813
search_key_name = bytesintern(info.get(b'search_key_name', b'plain'))
1814
parent_id_basename_to_file_id = bytesintern(info.get(
1815
b'parent_id_basename_to_file_id', None))
1816
if not parent_id_basename_to_file_id.startswith(b'sha1:'):
1921
revision_id = intern(info['revision_id'])
1922
root_id = intern(info['root_id'])
1923
search_key_name = intern(info.get('search_key_name', 'plain'))
1924
parent_id_basename_to_file_id = intern(info.get(
1925
'parent_id_basename_to_file_id', None))
1926
if not parent_id_basename_to_file_id.startswith('sha1:'):
1817
1927
raise ValueError('parent_id_basename_to_file_id should be a sha1'
1818
1928
' key not %r' % (parent_id_basename_to_file_id,))
1819
id_to_entry = info[b'id_to_entry']
1820
if not id_to_entry.startswith(b'sha1:'):
1929
id_to_entry = info['id_to_entry']
1930
if not id_to_entry.startswith('sha1:'):
1821
1931
raise ValueError('id_to_entry should be a sha1'
1822
1932
' key not %r' % (id_to_entry,))
1905
2015
return self._bytes_to_entry(
1906
next(self.id_to_entry.iteritems([StaticTuple(file_id,)]))[1])
2016
self.id_to_entry.iteritems([StaticTuple(file_id,)]).next()[1])
1907
2017
except StopIteration:
1908
2018
# really we're passing an inventory, not a tree...
1909
2019
raise errors.NoSuchId(self, file_id)
1911
2021
def _getitems(self, file_ids):
1912
"""Similar to get_entry, but lets you query for multiple.
2022
"""Similar to __getitem__, but lets you query for multiple.
1914
2024
The returned order is undefined. And currently if an item doesn't
1915
2025
exist, it isn't included in the output.
1943
2053
"""Yield the parents of file_id up to the root."""
1944
2054
while file_id is not None:
1946
ie = self.get_entry(file_id)
1947
2057
except KeyError:
1948
2058
raise errors.NoSuchId(tree=self, file_id=file_id)
1950
2060
file_id = ie.parent_id
1952
def iter_all_ids(self):
1953
2063
"""Iterate over all file-ids."""
1954
2064
for key, _ in self.id_to_entry.iteritems():
1957
2067
def iter_just_entries(self):
1958
2068
"""Iterate over all entries.
1960
2070
Unlike iter_entries(), just the entries are returned (not (path, ie))
1961
2071
and the order of entries is undefined.
1970
2080
self._fileid_to_entry_cache[file_id] = ie
1973
def _preload_cache(self):
1974
"""Make sure all file-ids are in _fileid_to_entry_cache"""
1975
if self._fully_cached:
1976
return # No need to do it again
1977
# The optimal sort order is to use iteritems() directly
1978
cache = self._fileid_to_entry_cache
1979
for key, entry in self.id_to_entry.iteritems():
1981
if file_id not in cache:
1982
ie = self._bytes_to_entry(entry)
1986
last_parent_id = last_parent_ie = None
1987
pid_items = self.parent_id_basename_to_file_id.iteritems()
1988
for key, child_file_id in pid_items:
1989
if key == (b'', b''): # This is the root
1990
if child_file_id != self.root_id:
1991
raise ValueError('Data inconsistency detected.'
1992
' We expected data with key ("","") to match'
1993
' the root id, but %s != %s'
1994
% (child_file_id, self.root_id))
1996
parent_id, basename = key
1997
ie = cache[child_file_id]
1998
if parent_id == last_parent_id:
1999
parent_ie = last_parent_ie
2001
parent_ie = cache[parent_id]
2002
if parent_ie.kind != 'directory':
2003
raise ValueError('Data inconsistency detected.'
2004
' An entry in the parent_id_basename_to_file_id map'
2005
' has parent_id {%s} but the kind of that object'
2006
' is %r not "directory"' % (parent_id, parent_ie.kind))
2007
if parent_ie._children is None:
2008
parent_ie._children = {}
2009
basename = basename.decode('utf-8')
2010
if basename in parent_ie._children:
2011
existing_ie = parent_ie._children[basename]
2012
if existing_ie != ie:
2013
raise ValueError('Data inconsistency detected.'
2014
' Two entries with basename %r were found'
2015
' in the parent entry {%s}'
2016
% (basename, parent_id))
2017
if basename != ie.name:
2018
raise ValueError('Data inconsistency detected.'
2019
' In the parent_id_basename_to_file_id map, file_id'
2020
' {%s} is listed as having basename %r, but in the'
2021
' id_to_entry map it is %r'
2022
% (child_file_id, basename, ie.name))
2023
parent_ie._children[basename] = ie
2024
self._fully_cached = True
2026
2083
def iter_changes(self, basis):
2027
2084
"""Generate a Tree.iter_changes change list between this and basis.
2125
2182
def path2id(self, relpath):
2126
2183
"""See CommonInventory.path2id()."""
2127
2184
# TODO: perhaps support negative hits?
2128
if isinstance(relpath, (str, text_type)):
2129
names = osutils.splitpath(relpath)
2134
relpath = osutils.pathjoin(*relpath)
2135
2185
result = self._path_to_fileid_cache.get(relpath, None)
2136
2186
if result is not None:
2188
if isinstance(relpath, basestring):
2189
names = osutils.splitpath(relpath)
2138
2192
current_id = self.root_id
2139
2193
if current_id is None:
2165
2219
def to_lines(self):
2166
2220
"""Serialise the inventory to lines."""
2167
lines = [b"chkinventory:\n"]
2168
if self._search_key_name != b'plain':
2221
lines = ["chkinventory:\n"]
2222
if self._search_key_name != 'plain':
2169
2223
# custom ordering grouping things that don't change together
2170
lines.append(b'search_key_name: %s\n' % (
2171
self._search_key_name))
2172
lines.append(b"root_id: %s\n" % self.root_id)
2173
lines.append(b'parent_id_basename_to_file_id: %s\n' %
2224
lines.append('search_key_name: %s\n' % (self._search_key_name,))
2225
lines.append("root_id: %s\n" % self.root_id)
2226
lines.append('parent_id_basename_to_file_id: %s\n' %
2174
2227
(self.parent_id_basename_to_file_id.key()[0],))
2175
lines.append(b"revision_id: %s\n" % self.revision_id)
2176
lines.append(b"id_to_entry: %s\n" % (self.id_to_entry.key()[0],))
2228
lines.append("revision_id: %s\n" % self.revision_id)
2229
lines.append("id_to_entry: %s\n" % (self.id_to_entry.key()[0],))
2178
lines.append(b"revision_id: %s\n" % self.revision_id)
2179
lines.append(b"root_id: %s\n" % self.root_id)
2231
lines.append("revision_id: %s\n" % self.revision_id)
2232
lines.append("root_id: %s\n" % self.root_id)
2180
2233
if self.parent_id_basename_to_file_id is not None:
2181
lines.append(b'parent_id_basename_to_file_id: %s\n' %
2234
lines.append('parent_id_basename_to_file_id: %s\n' %
2182
2235
(self.parent_id_basename_to_file_id.key()[0],))
2183
lines.append(b"id_to_entry: %s\n" % (self.id_to_entry.key()[0],))
2236
lines.append("id_to_entry: %s\n" % (self.id_to_entry.key()[0],))
2187
2240
def root(self):
2188
2241
"""Get the root entry."""
2189
return self.get_entry(self.root_id)
2242
return self[self.root_id]
2192
2245
class CHKInventoryDirectory(InventoryDirectory):
2193
2246
"""A directory in an inventory."""
2195
__slots__ = ['_children', '_chk_inventory']
2248
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
2249
'text_id', 'parent_id', '_children', 'executable',
2250
'revision', 'symlink_target', 'reference_revision',
2197
2253
def __init__(self, file_id, name, parent_id, chk_inventory):
2198
2254
# Don't call InventoryDirectory.__init__ - it isn't right for this
2200
2256
InventoryEntry.__init__(self, file_id, name, parent_id)
2201
2257
self._children = None
2258
self.kind = 'directory'
2202
2259
self._chk_inventory = chk_inventory
2387
2448
raise errors.InconsistentDelta(new_path, item[1],
2388
2449
"new_path with no entry")
2392
def mutable_inventory_from_tree(tree):
2393
"""Create a new inventory that has the same contents as a specified tree.
2395
:param tree: Revision tree to create inventory from
2397
entries = tree.iter_entries_by_dir()
2398
inv = Inventory(None, tree.get_revision_id())
2399
for path, inv_entry in entries:
2400
inv.add(inv_entry.copy())