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"
33
from collections.abc import deque
34
except ImportError: # python < 3.7
35
from collections import deque
38
from ..lazy_import import lazy_import
30
from bzrlib.lazy_import import lazy_import
39
31
lazy_import(globals(), """
45
from breezy.bzr import (
55
from ..sixish import (
61
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
64
57
class InventoryEntry(object):
94
87
>>> i = Inventory()
97
>>> i.add(InventoryDirectory(b'123', 'src', ROOT_ID))
90
>>> i.add(InventoryDirectory('123', 'src', ROOT_ID))
98
91
InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None)
99
>>> i.add(InventoryFile(b'2323', 'hello.c', parent_id='123'))
92
>>> i.add(InventoryFile('2323', 'hello.c', parent_id='123'))
100
93
InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None, revision=None)
101
94
>>> shouldbe = {0: '', 1: 'src', 2: 'src/hello.c'}
102
95
>>> for ix, j in enumerate(i.iter_entries()):
111
104
InventoryDirectory('2325', 'wibble', parent_id='123', revision=None)
112
105
>>> i.path2id('src/wibble')
114
109
>>> i.add(InventoryFile('2326', 'wibble.c', '2325'))
115
110
InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None, revision=None)
116
>>> i.get_entry('2326')
117
112
InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None, revision=None)
118
113
>>> for path, entry in i.iter_entries():
136
131
RENAMED = 'renamed'
137
132
MODIFIED_AND_RENAMED = 'modified and renamed'
139
__slots__ = ['file_id', 'revision', 'parent_id', 'name']
141
# Attributes that all InventoryEntry instances are expected to have, but
142
# that don't vary for all kinds of entry. (e.g. symlink_target is only
143
# relevant to InventoryLink, so there's no reason to make every
144
# InventoryFile instance allocate space to hold a value for it.)
145
# Attributes that only vary for files: executable, text_sha1, text_size,
151
# Attributes that only vary for symlinks: symlink_target
152
symlink_target = None
153
# Attributes that only vary for tree-references: reference_revision
154
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.
213
def __init__(self, file_id, name, parent_id):
200
def __init__(self, file_id, name, parent_id, text_id=None):
214
201
"""Create an InventoryEntry
216
203
The filename must be a single component, relative to the
217
204
parent directory; it cannot be a whole path or relative name.
219
>>> e = InventoryFile(b'123', 'hello.c', ROOT_ID)
206
>>> e = InventoryFile('123', 'hello.c', ROOT_ID)
224
>>> e = InventoryFile(b'123', 'src/hello.c', ROOT_ID)
211
>>> e = InventoryFile('123', 'src/hello.c', ROOT_ID)
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'))
331
347
if not isinstance(other, InventoryEntry):
332
348
return NotImplemented
334
return ((self.file_id == other.file_id) and
335
(self.name == other.name) and
336
(other.symlink_target == self.symlink_target) and
337
(self.text_sha1 == other.text_sha1) and
338
(self.text_size == other.text_size) and
339
(self.text_id == other.text_id) and
340
(self.parent_id == other.parent_id) and
341
(self.kind == other.kind) and
342
(self.revision == other.revision) and
343
(self.executable == other.executable) and
344
(self.reference_revision == other.reference_revision)
350
return ((self.file_id == other.file_id)
351
and (self.name == other.name)
352
and (other.symlink_target == self.symlink_target)
353
and (self.text_sha1 == other.text_sha1)
354
and (self.text_size == other.text_size)
355
and (self.text_id == other.text_id)
356
and (self.parent_id == other.parent_id)
357
and (self.kind == other.kind)
358
and (self.revision == other.revision)
359
and (self.executable == other.executable)
360
and (self.reference_revision == other.reference_revision)
347
363
def __ne__(self, other):
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
('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
('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(
445
497
'fileid {%s} in {%s} has None for text_size' % (self.file_id,
449
501
other = InventoryFile(self.file_id, self.name, self.parent_id)
461
513
return text_modified, meta_modified
463
515
def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
464
output_to, reverse=False):
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
% (self.file_id, tree_revision_id))
611
% (self.file_id, tree_revision_id))
546
612
# Symlinks are stored as ''
547
613
checker.add_pending_item(tree_revision_id,
548
('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
output_to, reverse=False):
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
654
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)
657
753
def has_filename(self, filename):
658
754
return bool(self.path2id(filename))
661
757
"""Return as a string the path to file_id.
663
759
>>> i = Inventory()
664
>>> e = i.add(InventoryDirectory(b'src-id', 'src', ROOT_ID))
665
>>> e = i.add(InventoryFile(b'foo-id', 'foo.c', parent_id='src-id'))
666
>>> 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')
669
765
:raises NoSuchId: If file_id is not present in the inventory.
686
782
from_dir = self.root
687
783
yield '', self.root
688
elif isinstance(from_dir, bytes):
689
from_dir = self.get_entry(from_dir)
784
elif isinstance(from_dir, basestring):
785
from_dir = self[from_dir]
691
787
# unrolling the recursive called changed the time from
692
788
# 440ms/663ms (inline/total) to 116ms/116ms
693
children = sorted(viewitems(from_dir.children))
789
children = from_dir.children.items()
694
791
if not recursive:
695
792
for name, ie in children:
698
children = deque(children)
795
children = collections.deque(children)
699
796
stack = [(u'', children)]
701
798
from_dir_relpath, children = stack[-1]
724
822
# if we finished all children, pop it off the stack
727
def _preload_cache(self):
728
"""Populate any caches, we are about to access all items.
730
The default implementation does nothing, because CommonInventory doesn't
735
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):
736
827
"""Iterate over the entries in a directory first order.
738
829
This returns all entries for a directory before returning
740
831
lexicographically sorted order, and is a hybrid between
741
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.
743
837
:return: This yields (path, entry) pairs
745
839
if specific_file_ids and not isinstance(specific_file_ids, set):
746
840
specific_file_ids = set(specific_file_ids)
747
841
# TODO? Perhaps this should return the from_dir so that the root is
748
842
# yielded? or maybe an option?
749
if from_dir is None and specific_file_ids is None:
750
# They are iterating from the root, and have not specified any
751
# specific entries to look at. All current callers fully consume the
752
# iterator, so we can safely assume we are accessing all entries
753
self._preload_cache()
754
843
if from_dir is None:
755
844
if self.root is None:
757
846
# Optimize a common case
758
if (specific_file_ids is not None
759
and len(specific_file_ids) == 1):
847
if (not yield_parents and specific_file_ids is not None and
848
len(specific_file_ids) == 1):
760
849
file_id = list(specific_file_ids)[0]
761
if file_id is not None:
763
path = self.id2path(file_id)
764
except errors.NoSuchId:
767
yield path, self.get_entry(file_id)
851
yield self.id2path(file_id), self[file_id]
769
853
from_dir = self.root
770
if (specific_file_ids is None
771
or self.root.file_id in specific_file_ids):
854
if (specific_file_ids is None or yield_parents or
855
self.root.file_id in specific_file_ids):
772
856
yield u'', self.root
773
elif isinstance(from_dir, bytes):
774
from_dir = self.get_entry(from_dir)
776
raise TypeError(from_dir)
857
elif isinstance(from_dir, basestring):
858
from_dir = self[from_dir]
778
860
if specific_file_ids is not None:
779
861
# TODO: jam 20070302 This could really be done as a loop rather
780
862
# than a bunch of recursive calls.
784
865
def add_ancestors(file_id):
785
if not byid.has_id(file_id):
866
if file_id not in byid:
787
parent_id = byid.get_entry(file_id).parent_id
868
parent_id = byid[file_id].parent_id
788
869
if parent_id is None:
790
871
if parent_id not in parents:
800
881
cur_relpath, cur_dir = stack.pop()
803
for child_name, child_ie in sorted(viewitems(cur_dir.children)):
884
for child_name, child_ie in sorted(cur_dir.children.iteritems()):
805
886
child_relpath = cur_relpath + child_name
807
if (specific_file_ids is None
808
or child_ie.file_id in specific_file_ids):
888
if (specific_file_ids is None or
889
child_ie.file_id in specific_file_ids or
890
(yield_parents and child_ie.file_id in parents)):
809
891
yield child_relpath, child_ie
811
893
if child_ie.kind == 'directory':
812
894
if parents is None or child_ie.file_id in parents:
813
child_dirs.append((child_relpath + '/', child_ie))
895
child_dirs.append((child_relpath+'/', child_ie))
814
896
stack.extend(reversed(child_dirs))
816
898
def _make_delta(self, old):
817
899
"""Make an inventory delta from two inventories."""
818
old_ids = set(old.iter_all_ids())
819
new_ids = set(self.iter_all_ids())
820
902
adds = new_ids - old_ids
821
903
deletes = old_ids - new_ids
822
904
common = old_ids.intersection(new_ids)
824
906
for file_id in deletes:
825
907
delta.append((old.id2path(file_id), None, file_id, None))
826
908
for file_id in adds:
827
delta.append((None, self.id2path(file_id),
828
file_id, self.get_entry(file_id)))
909
delta.append((None, self.id2path(file_id), file_id, self[file_id]))
829
910
for file_id in common:
830
if old.get_entry(file_id) != self.get_entry(file_id):
911
if old[file_id] != self[file_id]:
831
912
delta.append((old.id2path(file_id), self.id2path(file_id),
832
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)
835
924
def make_entry(self, kind, name, parent_id, file_id=None):
836
"""Simple thunk to breezy.bzr.inventory.make_entry."""
925
"""Simple thunk to bzrlib.inventory.make_entry."""
837
926
return make_entry(kind, name, parent_id, file_id)
839
928
def entries(self):
842
931
This may be faster than iter_entries.
846
934
def descend(dir_ie, dir_path):
847
kids = sorted(viewitems(dir_ie.children))
935
kids = dir_ie.children.items()
848
937
for name, ie in kids:
849
938
child_path = osutils.pathjoin(dir_path, name)
850
939
accum.append((child_path, ie))
851
940
if ie.kind == 'directory':
852
941
descend(ie, child_path)
854
if self.root is not None:
855
descend(self.root, u'')
858
def get_entry_by_path(self, relpath):
859
"""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.
861
965
:param relpath: may be either a list of path components, or a single
862
966
string, in which case it is automatically split.
889
993
# or raise an error?
893
def path2id(self, relpath):
894
"""Walk down through directories to return entry of last component.
896
:param relpath: may be either a list of path components, or a single
897
string, in which case it is automatically split.
899
This returns the entry of the last component in the path,
900
which may be either a file or a directory.
902
Returns None IFF the path is not found.
904
ie = self.get_entry_by_path(relpath)
996
return parent.file_id
909
998
def filter(self, specific_fileids):
910
999
"""Get an inventory view filtered against a set of file-ids.
924
1013
entries = self.iter_entries()
925
1014
if self.root is None:
926
1015
return Inventory(root_id=None)
927
other = Inventory(next(entries)[1].file_id)
1016
other = Inventory(entries.next()[1].file_id)
928
1017
other.root.revision = self.root.revision
929
1018
other.revision_id = self.revision_id
930
1019
directories_to_expand = set()
931
1020
for path, entry in entries:
932
1021
file_id = entry.file_id
933
if (file_id in specific_fileids or
934
entry.parent_id in directories_to_expand):
1022
if (file_id in specific_fileids
1023
or entry.parent_id in directories_to_expand):
935
1024
if entry.kind == 'directory':
936
1025
directories_to_expand.add(file_id)
937
1026
elif file_id not in interesting_parents:
962
1051
returned quickly.
964
1053
>>> inv = Inventory()
965
>>> inv.add(InventoryFile(b'123-123', 'hello.c', ROOT_ID))
1054
>>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
966
1055
InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
967
>>> inv.get_entry(b'123-123').name
1056
>>> inv['123-123'].name
970
1059
Id's may be looked up from paths:
972
1061
>>> inv.path2id('hello.c')
974
>>> inv.has_id(b'123-123')
1063
>>> '123-123' in inv
977
1066
There are iterators over the contents:
1019
1108
applied the final inventory must be internally consistent, but it
1020
1109
is ok to supply changes which, if only half-applied would have an
1021
1110
invalid result - such as supplying two changes which rename two
1022
files, 'A' and 'B' with each other : [('A', 'B', b'A-id', a_entry),
1023
('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)].
1025
1114
Each change is a tuple, of the form (old_path, new_path, file_id,
1065
1154
# after their children, which means that everything we examine has no
1066
1155
# modified children remaining by the time we examine it.
1067
1156
for old_path, file_id in sorted(((op, f) for op, np, f, e in delta
1068
if op is not None), reverse=True):
1157
if op is not None), reverse=True):
1069
1158
# Preserve unaltered children of file_id for later reinsertion.
1070
file_id_children = getattr(self.get_entry(file_id), 'children', {})
1159
file_id_children = getattr(self[file_id], 'children', {})
1071
1160
if len(file_id_children):
1072
1161
children[file_id] = file_id_children
1073
1162
if self.id2path(file_id) != old_path:
1074
1163
raise errors.InconsistentDelta(old_path, file_id,
1075
"Entry was at wrong other path %r." % self.id2path(file_id))
1164
"Entry was at wrong other path %r." % self.id2path(file_id))
1076
1165
# Remove file_id and the unaltered children. If file_id is not
1077
1166
# being deleted it will be reinserted back later.
1078
1167
self.remove_recursive_id(file_id)
1082
1171
# the resulting inventory were also modified, are inserted after their
1084
1173
for new_path, f, new_entry in sorted((np, f, e) for op, np, f, e in
1085
delta if np is not None):
1174
delta if np is not None):
1086
1175
if new_entry.kind == 'directory':
1087
1176
# Pop the child which to allow detection of children whose
1088
1177
# parents were deleted and which were not reattached to a new
1090
1179
replacement = InventoryDirectory(new_entry.file_id,
1091
new_entry.name, new_entry.parent_id)
1180
new_entry.name, new_entry.parent_id)
1092
1181
replacement.revision = new_entry.revision
1093
1182
replacement.children = children.pop(replacement.file_id, {})
1094
1183
new_entry = replacement
1096
1185
self.add(new_entry)
1097
1186
except errors.DuplicateFileId:
1098
1187
raise errors.InconsistentDelta(new_path, new_entry.file_id,
1099
"New id is already present in target.")
1188
"New id is already present in target.")
1100
1189
except AttributeError:
1101
1190
raise errors.InconsistentDelta(new_path, new_entry.file_id,
1102
"Parent is not a directory.")
1191
"Parent is not a directory.")
1103
1192
if self.id2path(new_entry.file_id) != new_path:
1104
1193
raise errors.InconsistentDelta(new_path, new_entry.file_id,
1105
"New path is not consistent with parent path.")
1194
"New path is not consistent with parent path.")
1106
1195
if len(children):
1107
1196
# Get the parent id that was deleted
1108
1197
parent_id, children = children.popitem()
1109
1198
raise errors.InconsistentDelta("<deleted>", parent_id,
1110
"The file id was deleted but its children were not deleted.")
1199
"The file id was deleted but its children were not deleted.")
1112
1201
def create_by_apply_delta(self, inventory_delta, new_revision_id,
1113
1202
propagate_caches=False):
1134
1223
other.add(entry.copy())
1137
def iter_all_ids(self):
1226
def _get_mutable_inventory(self):
1227
"""See CommonInventory._get_mutable_inventory."""
1228
return copy.deepcopy(self)
1138
1231
"""Iterate over all file-ids."""
1139
1232
return iter(self._byid)
1141
1234
def iter_just_entries(self):
1142
1235
"""Iterate over all entries.
1144
1237
Unlike iter_entries(), just the entries are returned (not (path, ie))
1145
1238
and the order of entries is undefined.
1147
1240
XXX: We may not want to merge this into bzr.dev.
1149
1242
if self.root is None:
1151
return iter(viewvalues(self._byid))
1244
for _, ie in self._byid.iteritems():
1153
1247
def __len__(self):
1154
1248
"""Returns number of entries."""
1155
1249
return len(self._byid)
1157
def get_entry(self, file_id):
1251
def __getitem__(self, file_id):
1158
1252
"""Return the entry for given file_id.
1160
1254
>>> inv = Inventory()
1161
>>> inv.add(InventoryFile(b'123123', 'hello.c', ROOT_ID))
1255
>>> inv.add(InventoryFile('123123', 'hello.c', ROOT_ID))
1162
1256
InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
1163
>>> inv.get_entry(b'123123').name
1257
>>> inv['123123'].name
1166
if not isinstance(file_id, bytes):
1167
raise TypeError(file_id)
1169
1261
return self._byid[file_id]
1170
1262
except KeyError:
1175
1267
return self._byid[file_id].kind
1177
1269
def get_child(self, parent_id, filename):
1178
return self.get_entry(parent_id).children.get(filename)
1270
return self[parent_id].children.get(filename)
1180
1272
def _add_child(self, entry):
1181
1273
"""Add an entry to the inventory, without adding it to its parent"""
1182
1274
if entry.file_id in self._byid:
1183
raise errors.BzrError(
1184
"inventory already contains entry with id {%s}" %
1275
raise BzrError("inventory already contains entry with id {%s}" %
1186
1277
self._byid[entry.file_id] = entry
1187
children = getattr(entry, 'children', {})
1188
if children is not None:
1189
for child in viewvalues(children):
1190
self._add_child(child)
1278
for child in getattr(entry, 'children', {}).itervalues():
1279
self._add_child(child)
1193
1282
def add(self, entry):
1194
1283
"""Add entry to inventory.
1285
To add a file to a branch ready to be committed, use Branch.add,
1198
1290
if entry.file_id in self._byid:
1237
1329
ie = make_entry(kind, parts[-1], parent_id, file_id)
1238
1330
return self.add(ie)
1240
def delete(self, file_id):
1332
def __delitem__(self, file_id):
1241
1333
"""Remove entry by id.
1243
1335
>>> inv = Inventory()
1244
>>> inv.add(InventoryFile(b'123', 'foo.c', ROOT_ID))
1336
>>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
1245
1337
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
1246
>>> inv.has_id(b'123')
1248
>>> inv.delete(b'123')
1249
>>> inv.has_id(b'123')
1252
ie = self.get_entry(file_id)
1253
1345
del self._byid[file_id]
1254
1346
if ie.parent_id is not None:
1255
del self.get_entry(ie.parent_id).children[ie.name]
1347
del self[ie.parent_id].children[ie.name]
1257
1349
def __eq__(self, other):
1258
1350
"""Compare two sets by comparing their contents.
1261
1353
>>> i2 = Inventory()
1264
>>> i1.add(InventoryFile(b'123', 'foo', ROOT_ID))
1356
>>> i1.add(InventoryFile('123', 'foo', ROOT_ID))
1265
1357
InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
1268
>>> i2.add(InventoryFile(b'123', 'foo', ROOT_ID))
1360
>>> i2.add(InventoryFile('123', 'foo', ROOT_ID))
1269
1361
InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
1297
1389
def _make_delta(self, old):
1298
1390
"""Make an inventory delta from two inventories."""
1299
old_getter = old.get_entry
1300
new_getter = self.get_entry
1301
old_ids = set(old.iter_all_ids())
1302
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)
1303
1395
adds = new_ids - old_ids
1304
1396
deletes = old_ids - new_ids
1305
1397
if not adds and not deletes:
1310
1402
for file_id in deletes:
1311
1403
delta.append((old.id2path(file_id), None, file_id, None))
1312
1404
for file_id in adds:
1313
delta.append((None, self.id2path(file_id),
1314
file_id, self.get_entry(file_id)))
1405
delta.append((None, self.id2path(file_id), file_id, self[file_id]))
1315
1406
for file_id in common:
1316
new_ie = new_getter(file_id)
1317
old_ie = old_getter(file_id)
1407
new_ie = new_getter[file_id]
1408
old_ie = old_getter[file_id]
1318
1409
# If xml_serializer returns the cached InventoryEntries (rather
1319
1410
# than always doing .copy()), inlining the 'is' check saves 2.7M
1320
1411
# calls to __eq__. Under lsprof this saves 20s => 6s.
1356
1447
new_name = ensure_normalized_name(new_name)
1357
1448
if not is_valid_name(new_name):
1358
raise errors.BzrError("not an acceptable filename: %r" % new_name)
1449
raise BzrError("not an acceptable filename: %r" % new_name)
1360
1451
new_parent = self._byid[new_parent_id]
1361
1452
if new_name in new_parent.children:
1362
raise errors.BzrError("%r already exists in %r" %
1363
(new_name, self.id2path(new_parent_id)))
1453
raise BzrError("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
1365
1455
new_parent_idpath = self.get_idpath(new_parent_id)
1366
1456
if file_id in new_parent_idpath:
1367
raise errors.BzrError(
1368
"cannot move directory %r into a subdirectory of itself, %r"
1369
% (self.id2path(file_id), self.id2path(new_parent_id)))
1457
raise BzrError("cannot move directory %r into a subdirectory of itself, %r"
1458
% (self.id2path(file_id), self.id2path(new_parent_id)))
1371
1460
file_ie = self._byid[file_id]
1372
1461
old_parent = self._byid[file_ie.parent_id]
1443
1531
if entry.parent_id is not None:
1444
1532
parent_str = entry.parent_id
1447
1535
name_str = entry.name.encode("utf8")
1448
1536
if entry.kind == 'file':
1449
1537
if entry.executable:
1453
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" % (
1454
1542
entry.file_id, parent_str, name_str, entry.revision,
1455
1543
entry.text_sha1, entry.text_size, exec_str)
1456
1544
elif entry.kind == 'directory':
1457
return b"dir: %s\n%s\n%s\n%s" % (
1545
return "dir: %s\n%s\n%s\n%s" % (
1458
1546
entry.file_id, parent_str, name_str, entry.revision)
1459
1547
elif entry.kind == 'symlink':
1460
return b"symlink: %s\n%s\n%s\n%s\n%s" % (
1548
return "symlink: %s\n%s\n%s\n%s\n%s" % (
1461
1549
entry.file_id, parent_str, name_str, entry.revision,
1462
1550
entry.symlink_target.encode("utf8"))
1463
1551
elif entry.kind == 'tree-reference':
1464
return b"tree: %s\n%s\n%s\n%s\n%s" % (
1552
return "tree: %s\n%s\n%s\n%s\n%s" % (
1465
1553
entry.file_id, parent_str, name_str, entry.revision,
1466
1554
entry.reference_revision)
1509
1597
remaining_parents = interesting.difference(file_ids)
1510
1598
# When we hit the TREE_ROOT, we'll get an interesting parent of None,
1511
1599
# but we don't actually want to recurse into that
1512
interesting.add(None) # this will auto-filter it in the loop
1513
remaining_parents.discard(None)
1600
interesting.add(None) # this will auto-filter it in the loop
1601
remaining_parents.discard(None)
1514
1602
while remaining_parents:
1515
1603
next_parents = set()
1516
1604
for entry in self._getitems(remaining_parents):
1517
1605
next_parents.add(entry.parent_id)
1518
children_of_parent_id.setdefault(entry.parent_id, set()
1519
).add(entry.file_id)
1606
children_of_parent_id.setdefault(entry.parent_id, []
1607
).append(entry.file_id)
1520
1608
# Remove any search tips we've already processed
1521
1609
remaining_parents = next_parents.difference(interesting)
1522
1610
interesting.update(remaining_parents)
1529
1617
keys = [StaticTuple(f,).intern() for f in directories_to_expand]
1530
1618
directories_to_expand = set()
1531
1619
items = self.parent_id_basename_to_file_id.iteritems(keys)
1532
next_file_ids = {item[1] for item in items}
1620
next_file_ids = set([item[1] for item in items])
1533
1621
next_file_ids = next_file_ids.difference(interesting)
1534
1622
interesting.update(next_file_ids)
1535
1623
for entry in self._getitems(next_file_ids):
1536
1624
if entry.kind == 'directory':
1537
1625
directories_to_expand.add(entry.file_id)
1538
children_of_parent_id.setdefault(entry.parent_id, set()
1539
).add(entry.file_id)
1626
children_of_parent_id.setdefault(entry.parent_id, []
1627
).append(entry.file_id)
1540
1628
return interesting, children_of_parent_id
1542
1630
def filter(self, specific_fileids):
1564
1652
# parent_to_children with at least the tree root.)
1566
1654
cache = self._fileid_to_entry_cache
1567
remaining_children = deque(
1568
parent_to_children[self.root_id])
1656
remaining_children = collections.deque(parent_to_children[self.root_id])
1658
import pdb; pdb.set_trace()
1569
1660
while remaining_children:
1570
1661
file_id = remaining_children.popleft()
1571
1662
ie = cache[file_id]
1572
1663
if ie.kind == 'directory':
1573
ie = ie.copy() # We create a copy to depopulate the .children attribute
1664
ie = ie.copy() # We create a copy to depopulate the .children attribute
1574
1665
# TODO: depending on the uses of 'other' we should probably alwyas
1575
1666
# '.copy()' to prevent someone from mutating other and
1576
1667
# invaliding our internal cache
1583
def _bytes_to_utf8name_key(data):
1584
"""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."""
1585
1676
# We don't normally care about name, except for times when we want
1586
1677
# to filter out empty names because of non rich-root...
1587
sections = data.split(b'\n')
1588
kind, file_id = sections[0].split(b': ')
1589
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]))
1591
1682
def _bytes_to_entry(self, bytes):
1592
1683
"""Deserialise a serialised entry."""
1593
sections = bytes.split(b'\n')
1594
if sections[0].startswith(b"file: "):
1684
sections = bytes.split('\n')
1685
if sections[0].startswith("file: "):
1595
1686
result = InventoryFile(sections[0][6:],
1596
sections[2].decode('utf8'),
1687
sections[2].decode('utf8'),
1598
1689
result.text_sha1 = sections[4]
1599
1690
result.text_size = int(sections[5])
1600
result.executable = sections[6] == b"Y"
1601
elif sections[0].startswith(b"dir: "):
1691
result.executable = sections[6] == "Y"
1692
elif sections[0].startswith("dir: "):
1602
1693
result = CHKInventoryDirectory(sections[0][5:],
1603
sections[2].decode('utf8'),
1605
elif sections[0].startswith(b"symlink: "):
1694
sections[2].decode('utf8'),
1696
elif sections[0].startswith("symlink: "):
1606
1697
result = InventoryLink(sections[0][9:],
1607
sections[2].decode('utf8'),
1698
sections[2].decode('utf8'),
1609
1700
result.symlink_target = sections[4].decode('utf8')
1610
elif sections[0].startswith(b"tree: "):
1701
elif sections[0].startswith("tree: "):
1611
1702
result = TreeReference(sections[0][6:],
1612
sections[2].decode('utf8'),
1703
sections[2].decode('utf8'),
1614
1705
result.reference_revision = sections[4]
1616
1707
raise ValueError("Not a serialised entry %r" % bytes)
1617
result.file_id = bytesintern(result.file_id)
1618
result.revision = bytesintern(sections[3])
1619
if result.parent_id == b'':
1708
result.file_id = intern(result.file_id)
1709
result.revision = intern(sections[3])
1710
if result.parent_id == '':
1620
1711
result.parent_id = None
1621
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())
1624
1723
def create_by_apply_delta(self, inventory_delta, new_revision_id,
1625
propagate_caches=False):
1724
propagate_caches=False):
1626
1725
"""Create a new CHKInventory by applying inventory_delta to this one.
1628
1727
See the inventory developers documentation for the theory behind
1754
1852
new_key, [None, None])[1] = new_value
1755
1853
# validate that deletes are complete.
1756
1854
for file_id in deletes:
1757
entry = self.get_entry(file_id)
1855
entry = self[file_id]
1758
1856
if entry.kind != 'directory':
1760
1858
# This loop could potentially be better by using the id_basename
1761
1859
# map to just get the child file ids.
1762
for child in viewvalues(entry.children):
1860
for child in entry.children.values():
1763
1861
if child.file_id not in altered:
1764
1862
raise errors.InconsistentDelta(self.id2path(child.file_id),
1765
child.file_id, "Child not deleted or reparented when "
1863
child.file_id, "Child not deleted or reparented when "
1767
1865
result.id_to_entry.apply_delta(id_to_entry_delta)
1768
1866
if parent_id_basename_delta:
1769
1867
# Transform the parent_id_basename delta data into a linear delta
1780
1878
parents.discard(('', None))
1781
1879
for parent_path, parent in parents:
1783
if result.get_entry(parent).kind != 'directory':
1881
if result[parent].kind != 'directory':
1784
1882
raise errors.InconsistentDelta(result.id2path(parent), parent,
1785
'Not a directory, but given children')
1883
'Not a directory, but given children')
1786
1884
except errors.NoSuchId:
1787
1885
raise errors.InconsistentDelta("<unknown>", parent,
1788
"Parent is not present in resulting inventory.")
1886
"Parent is not present in resulting inventory.")
1789
1887
if result.path2id(parent_path) != parent:
1790
1888
raise errors.InconsistentDelta(parent_path, parent,
1791
"Parent has wrong path %r." % result.path2id(parent_path))
1889
"Parent has wrong path %r." % result.path2id(parent_path))
1802
1900
:return: A CHKInventory
1804
lines = bytes.split(b'\n')
1805
if lines[-1] != b'':
1902
lines = bytes.split('\n')
1806
1904
raise AssertionError('bytes to deserialize must end with an eol')
1808
if lines[0] != b'chkinventory:':
1906
if lines[0] != 'chkinventory:':
1809
1907
raise ValueError("not a serialised CHKInventory: %r" % bytes)
1811
allowed_keys = frozenset((b'root_id', b'revision_id',
1812
b'parent_id_basename_to_file_id',
1813
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',
1814
1912
for line in lines[1:]:
1815
key, value = line.split(b': ', 1)
1913
key, value = line.split(': ', 1)
1816
1914
if key not in allowed_keys:
1817
1915
raise errors.BzrError('Unknown key in inventory: %r\n%r'
1818
1916
% (key, bytes))
1820
1918
raise errors.BzrError('Duplicate key in inventory: %r\n%r'
1821
1919
% (key, bytes))
1822
1920
info[key] = value
1823
revision_id = bytesintern(info[b'revision_id'])
1824
root_id = bytesintern(info[b'root_id'])
1825
search_key_name = bytesintern(info.get(b'search_key_name', b'plain'))
1826
parent_id_basename_to_file_id = bytesintern(info.get(
1827
b'parent_id_basename_to_file_id', None))
1828
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:'):
1829
1927
raise ValueError('parent_id_basename_to_file_id should be a sha1'
1830
1928
' key not %r' % (parent_id_basename_to_file_id,))
1831
id_to_entry = info[b'id_to_entry']
1832
if not id_to_entry.startswith(b'sha1:'):
1929
id_to_entry = info['id_to_entry']
1930
if not id_to_entry.startswith('sha1:'):
1833
1931
raise ValueError('id_to_entry should be a sha1'
1834
1932
' key not %r' % (id_to_entry,))
1850
1948
search_key_func=search_key_func)
1851
1949
if (result.revision_id,) != expected_revision_id:
1852
1950
raise ValueError("Mismatched revision id and expected: %r, %r" %
1853
(result.revision_id, expected_revision_id))
1951
(result.revision_id, expected_revision_id))
1857
def from_inventory(klass, chk_store, inventory, maximum_size=0, search_key_name=b'plain'):
1955
def from_inventory(klass, chk_store, inventory, maximum_size=0, search_key_name='plain'):
1858
1956
"""Create a CHKInventory from an existing inventory.
1860
1958
The content of inventory is copied into the chk_store, and a
1880
1978
parent_id_basename_dict[p_id_key] = entry.file_id
1882
1980
result._populate_from_dicts(chk_store, id_to_entry_dict,
1883
parent_id_basename_dict, maximum_size=maximum_size)
1981
parent_id_basename_dict, maximum_size=maximum_size)
1886
1984
def _populate_from_dicts(self, chk_store, id_to_entry_dict,
1887
1985
parent_id_basename_dict, maximum_size):
1888
search_key_func = chk_map.search_key_registry.get(
1889
self._search_key_name)
1986
search_key_func = chk_map.search_key_registry.get(self._search_key_name)
1890
1987
root_key = chk_map.CHKMap.from_dict(chk_store, id_to_entry_dict,
1891
maximum_size=maximum_size, key_width=1,
1892
search_key_func=search_key_func)
1988
maximum_size=maximum_size, key_width=1,
1989
search_key_func=search_key_func)
1893
1990
self.id_to_entry = chk_map.CHKMap(chk_store, root_key,
1894
1991
search_key_func)
1895
1992
root_key = chk_map.CHKMap.from_dict(chk_store,
1896
parent_id_basename_dict,
1897
maximum_size=maximum_size, key_width=2,
1898
search_key_func=search_key_func)
1993
parent_id_basename_dict,
1994
maximum_size=maximum_size, key_width=2,
1995
search_key_func=search_key_func)
1899
1996
self.parent_id_basename_to_file_id = chk_map.CHKMap(chk_store,
1900
root_key, search_key_func)
1997
root_key, search_key_func)
1902
1999
def _parent_id_basename_key(self, entry):
1903
2000
"""Create a key for a entry in a parent_id_basename_to_file_id index."""
1904
2001
if entry.parent_id is not None:
1905
2002
parent_id = entry.parent_id
1908
2005
return StaticTuple(parent_id, entry.name.encode('utf8')).intern()
1910
def get_entry(self, file_id):
2007
def __getitem__(self, file_id):
1911
2008
"""map a single file_id -> InventoryEntry."""
1912
2009
if file_id is None:
1913
2010
raise errors.NoSuchId(self, file_id)
1918
2015
return self._bytes_to_entry(
1919
next(self.id_to_entry.iteritems([StaticTuple(file_id,)]))[1])
2016
self.id_to_entry.iteritems([StaticTuple(file_id,)]).next()[1])
1920
2017
except StopIteration:
1921
2018
# really we're passing an inventory, not a tree...
1922
2019
raise errors.NoSuchId(self, file_id)
1924
2021
def _getitems(self, file_ids):
1925
"""Similar to get_entry, but lets you query for multiple.
2022
"""Similar to __getitem__, but lets you query for multiple.
1927
2024
The returned order is undefined. And currently if an item doesn't
1928
2025
exist, it isn't included in the output.
1956
2053
"""Yield the parents of file_id up to the root."""
1957
2054
while file_id is not None:
1959
ie = self.get_entry(file_id)
1960
2057
except KeyError:
1961
2058
raise errors.NoSuchId(tree=self, file_id=file_id)
1963
2060
file_id = ie.parent_id
1965
def iter_all_ids(self):
1966
2063
"""Iterate over all file-ids."""
1967
2064
for key, _ in self.id_to_entry.iteritems():
1970
2067
def iter_just_entries(self):
1971
2068
"""Iterate over all entries.
1973
2070
Unlike iter_entries(), just the entries are returned (not (path, ie))
1974
2071
and the order of entries is undefined.
1983
2080
self._fileid_to_entry_cache[file_id] = ie
1986
def _preload_cache(self):
1987
"""Make sure all file-ids are in _fileid_to_entry_cache"""
1988
if self._fully_cached:
1989
return # No need to do it again
1990
# The optimal sort order is to use iteritems() directly
1991
cache = self._fileid_to_entry_cache
1992
for key, entry in self.id_to_entry.iteritems():
1994
if file_id not in cache:
1995
ie = self._bytes_to_entry(entry)
1999
last_parent_id = last_parent_ie = None
2000
pid_items = self.parent_id_basename_to_file_id.iteritems()
2001
for key, child_file_id in pid_items:
2002
if key == (b'', b''): # This is the root
2003
if child_file_id != self.root_id:
2004
raise ValueError('Data inconsistency detected.'
2005
' We expected data with key ("","") to match'
2006
' the root id, but %s != %s'
2007
% (child_file_id, self.root_id))
2009
parent_id, basename = key
2010
ie = cache[child_file_id]
2011
if parent_id == last_parent_id:
2012
parent_ie = last_parent_ie
2014
parent_ie = cache[parent_id]
2015
if parent_ie.kind != 'directory':
2016
raise ValueError('Data inconsistency detected.'
2017
' An entry in the parent_id_basename_to_file_id map'
2018
' has parent_id {%s} but the kind of that object'
2019
' is %r not "directory"' % (parent_id, parent_ie.kind))
2020
if parent_ie._children is None:
2021
parent_ie._children = {}
2022
basename = basename.decode('utf-8')
2023
if basename in parent_ie._children:
2024
existing_ie = parent_ie._children[basename]
2025
if existing_ie != ie:
2026
raise ValueError('Data inconsistency detected.'
2027
' Two entries with basename %r were found'
2028
' in the parent entry {%s}'
2029
% (basename, parent_id))
2030
if basename != ie.name:
2031
raise ValueError('Data inconsistency detected.'
2032
' In the parent_id_basename_to_file_id map, file_id'
2033
' {%s} is listed as having basename %r, but in the'
2034
' id_to_entry map it is %r'
2035
% (child_file_id, basename, ie.name))
2036
parent_ie._children[basename] = ie
2037
self._fully_cached = True
2039
2083
def iter_changes(self, basis):
2040
2084
"""Generate a Tree.iter_changes change list between this and basis.
2086
2130
if kind[0] != kind[1]:
2087
2131
changed_content = True
2088
2132
elif kind[0] == 'file':
2089
if (self_entry.text_size != basis_entry.text_size
2090
or self_entry.text_sha1 != basis_entry.text_sha1):
2133
if (self_entry.text_size != basis_entry.text_size or
2134
self_entry.text_sha1 != basis_entry.text_sha1):
2091
2135
changed_content = True
2092
2136
elif kind[0] == 'symlink':
2093
2137
if self_entry.symlink_target != basis_entry.symlink_target:
2094
2138
changed_content = True
2095
2139
elif kind[0] == 'tree-reference':
2096
if (self_entry.reference_revision
2097
!= basis_entry.reference_revision):
2140
if (self_entry.reference_revision !=
2141
basis_entry.reference_revision):
2098
2142
changed_content = True
2099
2143
parent = (basis_parent, self_parent)
2100
2144
name = (basis_name, self_name)
2101
2145
executable = (basis_executable, self_executable)
2102
if (not changed_content and
2103
parent[0] == parent[1] and
2104
name[0] == name[1] and
2105
executable[0] == executable[1]):
2146
if (not changed_content
2147
and parent[0] == parent[1]
2148
and name[0] == name[1]
2149
and executable[0] == executable[1]):
2106
2150
# Could happen when only the revision changed for a directory
2107
2151
# for instance.
2109
2153
yield (file_id, (path_in_source, path_in_target), changed_content,
2110
versioned, parent, name, kind, executable)
2154
versioned, parent, name, kind, executable)
2112
2156
def __len__(self):
2113
2157
"""Return the number of entries in the inventory."""
2116
2160
def _make_delta(self, old):
2117
2161
"""Make an inventory delta from two inventories."""
2118
if not isinstance(old, CHKInventory):
2162
if type(old) != CHKInventory:
2119
2163
return CommonInventory._make_delta(self, old)
2121
2165
for key, old_value, self_value in \
2122
self.id_to_entry.iter_changes(old.id_to_entry):
2166
self.id_to_entry.iter_changes(old.id_to_entry):
2123
2167
file_id = key[0]
2124
2168
if old_value is not None:
2125
2169
old_path = old.id2path(file_id)
2138
2182
def path2id(self, relpath):
2139
2183
"""See CommonInventory.path2id()."""
2140
2184
# TODO: perhaps support negative hits?
2141
if isinstance(relpath, (str, text_type)):
2142
names = osutils.splitpath(relpath)
2147
relpath = osutils.pathjoin(*relpath)
2148
2185
result = self._path_to_fileid_cache.get(relpath, None)
2149
2186
if result is not None:
2188
if isinstance(relpath, basestring):
2189
names = osutils.splitpath(relpath)
2151
2192
current_id = self.root_id
2152
2193
if current_id is None:
2178
2219
def to_lines(self):
2179
2220
"""Serialise the inventory to lines."""
2180
lines = [b"chkinventory:\n"]
2181
if self._search_key_name != b'plain':
2221
lines = ["chkinventory:\n"]
2222
if self._search_key_name != 'plain':
2182
2223
# custom ordering grouping things that don't change together
2183
lines.append(b'search_key_name: %s\n' % (
2184
self._search_key_name))
2185
lines.append(b"root_id: %s\n" % self.root_id)
2186
lines.append(b'parent_id_basename_to_file_id: %s\n' %
2187
(self.parent_id_basename_to_file_id.key()[0],))
2188
lines.append(b"revision_id: %s\n" % self.revision_id)
2189
lines.append(b"id_to_entry: %s\n" % (self.id_to_entry.key()[0],))
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' %
2227
(self.parent_id_basename_to_file_id.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],))
2191
lines.append(b"revision_id: %s\n" % self.revision_id)
2192
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)
2193
2233
if self.parent_id_basename_to_file_id is not None:
2194
lines.append(b'parent_id_basename_to_file_id: %s\n' %
2195
(self.parent_id_basename_to_file_id.key()[0],))
2196
lines.append(b"id_to_entry: %s\n" % (self.id_to_entry.key()[0],))
2234
lines.append('parent_id_basename_to_file_id: %s\n' %
2235
(self.parent_id_basename_to_file_id.key()[0],))
2236
lines.append("id_to_entry: %s\n" % (self.id_to_entry.key()[0],))
2200
2240
def root(self):
2201
2241
"""Get the root entry."""
2202
return self.get_entry(self.root_id)
2242
return self[self.root_id]
2205
2245
class CHKInventoryDirectory(InventoryDirectory):
2206
2246
"""A directory in an inventory."""
2208
__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',
2210
2253
def __init__(self, file_id, name, parent_id, chk_inventory):
2211
2254
# Don't call InventoryDirectory.__init__ - it isn't right for this
2213
2256
InventoryEntry.__init__(self, file_id, name, parent_id)
2214
2257
self._children = None
2258
self.kind = 'directory'
2215
2259
self._chk_inventory = chk_inventory
2229
2273
# No longer supported
2230
2274
if self._chk_inventory.parent_id_basename_to_file_id is None:
2231
2275
raise AssertionError("Inventories without"
2232
" parent_id_basename_to_file_id are no longer supported")
2276
" parent_id_basename_to_file_id are no longer supported")
2234
2278
# XXX: Todo - use proxy objects for the children rather than loading
2235
2279
# all when the attribute is referenced.
2236
2280
parent_id_index = self._chk_inventory.parent_id_basename_to_file_id
2237
2281
child_keys = set()
2238
2282
for (parent_id, name_utf8), file_id in parent_id_index.iteritems(
2239
key_filter=[StaticTuple(self.file_id,)]):
2283
key_filter=[StaticTuple(self.file_id,)]):
2240
2284
child_keys.add(StaticTuple(file_id,))
2242
2286
for file_id_key in child_keys:
2398
2443
entry = item[3]
2399
2444
if new_path is None and entry is not None:
2400
2445
raise errors.InconsistentDelta(item[0], item[1],
2401
"Entry with no new_path")
2446
"Entry with no new_path")
2402
2447
if new_path is not None and entry is None:
2403
2448
raise errors.InconsistentDelta(new_path, item[1],
2404
"new_path with no entry")
2449
"new_path with no entry")
2408
def mutable_inventory_from_tree(tree):
2409
"""Create a new inventory that has the same contents as a specified tree.
2411
:param tree: Revision tree to create inventory from
2413
entries = tree.iter_entries_by_dir()
2414
inv = Inventory(None, tree.get_revision_id())
2415
for path, inv_entry in entries:
2416
inv.add(inv_entry.copy())