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(), """
41
from breezy.bzr import (
53
from ..sixish import (
59
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
62
57
class InventoryEntry(object):
92
87
>>> i = Inventory()
95
>>> i.add(InventoryDirectory(b'123', 'src', ROOT_ID))
90
>>> i.add(InventoryDirectory('123', 'src', ROOT_ID))
96
91
InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None)
97
>>> i.add(InventoryFile(b'2323', 'hello.c', parent_id='123'))
92
>>> i.add(InventoryFile('2323', 'hello.c', parent_id='123'))
98
93
InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None, revision=None)
99
94
>>> shouldbe = {0: '', 1: 'src', 2: 'src/hello.c'}
100
95
>>> for ix, j in enumerate(i.iter_entries()):
109
104
InventoryDirectory('2325', 'wibble', parent_id='123', revision=None)
110
105
>>> i.path2id('src/wibble')
112
109
>>> i.add(InventoryFile('2326', 'wibble.c', '2325'))
113
110
InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None, revision=None)
114
>>> i.get_entry('2326')
115
112
InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None, revision=None)
116
113
>>> for path, entry in i.iter_entries():
134
131
RENAMED = 'renamed'
135
132
MODIFIED_AND_RENAMED = 'modified and renamed'
137
__slots__ = ['file_id', 'revision', 'parent_id', 'name']
139
# Attributes that all InventoryEntry instances are expected to have, but
140
# that don't vary for all kinds of entry. (e.g. symlink_target is only
141
# relevant to InventoryLink, so there's no reason to make every
142
# InventoryFile instance allocate space to hold a value for it.)
143
# Attributes that only vary for files: executable, text_sha1, text_size,
149
# Attributes that only vary for symlinks: symlink_target
150
symlink_target = None
151
# Attributes that only vary for tree-references: reference_revision
152
reference_revision = None
154
136
def detect_changes(self, old_entry):
155
137
"""Return a (text_modified, meta_modified) from this to old_entry.
197
176
candidates[ie.revision] = ie
198
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)
200
189
def has_text(self):
201
190
"""Return true if the object this entry represents has textual data.
211
def __init__(self, file_id, name, parent_id):
200
def __init__(self, file_id, name, parent_id, text_id=None):
212
201
"""Create an InventoryEntry
214
203
The filename must be a single component, relative to the
215
204
parent directory; it cannot be a whole path or relative name.
217
>>> e = InventoryFile(b'123', 'hello.c', ROOT_ID)
206
>>> e = InventoryFile('123', 'hello.c', ROOT_ID)
222
>>> e = InventoryFile(b'123', 'src/hello.c', ROOT_ID)
211
>>> e = InventoryFile('123', 'src/hello.c', ROOT_ID)
223
212
Traceback (most recent call last):
224
213
InvalidEntryName: Invalid entry name: src/hello.c
215
if '/' in name or '\\' in name:
227
216
raise errors.InvalidEntryName(name=name)
228
if not isinstance(file_id, bytes):
229
raise TypeError(file_id)
217
self.executable = False
219
self.text_sha1 = None
220
self.text_size = None
230
221
self.file_id = file_id
223
self.text_id = text_id
233
224
self.parent_id = parent_id
225
self.symlink_target = None
226
self.reference_revision = None
235
228
def kind_character(self):
236
229
"""Return a short kind indicator useful for appending to names."""
237
raise errors.BzrError('unknown kind %r' % self.kind)
230
raise BzrError('unknown kind %r' % self.kind)
239
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())
242
261
def versionable_kind(kind):
243
262
return (kind in ('file', 'directory', 'symlink', 'tree-reference'))
335
347
if not isinstance(other, InventoryEntry):
336
348
return NotImplemented
338
return ((self.file_id == other.file_id) and
339
(self.name == other.name) and
340
(other.symlink_target == self.symlink_target) and
341
(self.text_sha1 == other.text_sha1) and
342
(self.text_size == other.text_size) and
343
(self.text_id == other.text_id) and
344
(self.parent_id == other.parent_id) and
345
(self.kind == other.kind) and
346
(self.revision == other.revision) and
347
(self.executable == other.executable) and
348
(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)
351
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)
388
428
class InventoryDirectory(InventoryEntry):
389
429
"""A directory in an inventory."""
391
__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']
395
435
def _check(self, checker, rev_id):
396
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))
397
441
# In non rich root repositories we do not expect a file graph for the
399
443
if self.name == '' and not checker.rich_roots:
402
446
# to provide a per-fileid log. The hash of every directory content is
403
447
# "da..." below (the sha1sum of '').
404
448
checker.add_pending_item(rev_id,
405
('texts', self.file_id, self.revision), b'text',
406
b'da39a3ee5e6b4b0d3255bfef95601890afd80709')
449
('texts', self.file_id, self.revision), 'text',
450
'da39a3ee5e6b4b0d3255bfef95601890afd80709')
409
453
other = InventoryDirectory(self.file_id, self.name, self.parent_id)
415
459
def __init__(self, file_id, name, parent_id):
416
460
super(InventoryDirectory, self).__init__(file_id, name, parent_id)
417
461
self.children = {}
419
def sorted_children(self):
420
return sorted(viewitems(self.children))
462
self.kind = 'directory'
422
464
def kind_character(self):
423
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."""
427
482
class InventoryFile(InventoryEntry):
428
483
"""A file in an inventory."""
430
__slots__ = ['text_sha1', 'text_size', 'text_id', 'executable']
434
def __init__(self, file_id, name, parent_id):
435
super(InventoryFile, self).__init__(file_id, name, parent_id)
436
self.text_sha1 = None
437
self.text_size = None
439
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']
441
489
def _check(self, checker, tree_revision_id):
442
490
"""See InventoryEntry._check"""
443
491
# TODO: check size too.
444
492
checker.add_pending_item(tree_revision_id,
445
('texts', self.file_id, self.revision), b'text',
493
('texts', self.file_id, self.revision), 'text',
447
495
if self.text_size is None:
448
496
checker._report_items.append(
449
497
'fileid {%s} in {%s} has None for text_size' % (self.file_id,
453
501
other = InventoryFile(self.file_id, self.name, self.parent_id)
465
513
return text_modified, meta_modified
467
515
def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
468
output_to, reverse=False):
516
output_to, reverse=False):
469
517
"""See InventoryEntry._diff."""
470
from breezy.diff import DiffText
518
from bzrlib.diff import DiffText
471
519
from_file_id = self.file_id
473
521
to_file_id = to_entry.file_id
474
to_path = to_tree.id2path(to_file_id)
476
523
to_file_id = None
478
if from_file_id is not None:
479
from_path = tree.id2path(from_file_id)
483
525
to_file_id, from_file_id = from_file_id, to_file_id
484
526
tree, to_tree = to_tree, tree
485
527
from_label, to_label = to_label, from_label
486
528
differ = DiffText(tree, to_tree, output_to, 'utf-8', '', '',
488
return differ.diff_text(from_path, to_path, from_label, to_label,
489
from_file_id, to_file_id)
530
return differ.diff_text(from_file_id, to_file_id, from_label, to_label)
491
532
def has_text(self):
492
533
"""See InventoryEntry.has_text."""
536
def __init__(self, file_id, name, parent_id):
537
super(InventoryFile, self).__init__(file_id, name, parent_id)
495
540
def kind_character(self):
496
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)
499
561
def _read_tree_state(self, path, work_tree):
500
562
"""See InventoryEntry._read_tree_state."""
501
self.text_sha1 = work_tree.get_file_sha1(path)
563
self.text_sha1 = work_tree.get_file_sha1(self.file_id, path=path)
502
564
# FIXME: 20050930 probe for the text size when getting sha1
503
565
# in _read_tree_state
504
self.executable = work_tree.is_executable(path)
566
self.executable = work_tree.is_executable(self.file_id, path=path)
506
568
def __repr__(self):
507
569
return ("%s(%r, %r, parent_id=%r, sha1=%r, len=%s, revision=%s)"
533
595
class InventoryLink(InventoryEntry):
534
596
"""A file in an inventory."""
536
__slots__ = ['symlink_target']
540
def __init__(self, file_id, name, parent_id):
541
super(InventoryLink, self).__init__(file_id, name, parent_id)
542
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']
544
602
def _check(self, checker, tree_revision_id):
545
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))
546
608
if self.symlink_target is None:
547
609
checker._report_items.append(
548
610
'symlink {%s} has no target in revision {%s}'
549
% (self.file_id, tree_revision_id))
611
% (self.file_id, tree_revision_id))
550
612
# Symlinks are stored as ''
551
613
checker.add_pending_item(tree_revision_id,
552
('texts', self.file_id, self.revision), b'text',
553
b'da39a3ee5e6b4b0d3255bfef95601890afd80709')
614
('texts', self.file_id, self.revision), 'text',
615
'da39a3ee5e6b4b0d3255bfef95601890afd80709')
556
618
other = InventoryLink(self.file_id, self.name, self.parent_id)
563
625
# FIXME: which _modified field should we use ? RBC 20051003
564
626
text_modified = (self.symlink_target != old_entry.symlink_target)
565
627
if text_modified:
566
trace.mutter(" symlink target changed")
628
mutter(" symlink target changed")
567
629
meta_modified = False
568
630
return text_modified, meta_modified
570
632
def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
571
output_to, reverse=False):
633
output_to, reverse=False):
572
634
"""See InventoryEntry._diff."""
573
from breezy.diff import DiffSymlink
635
from bzrlib.diff import DiffSymlink
574
636
old_target = self.symlink_target
575
637
if to_entry is not None:
576
638
new_target = to_entry.symlink_target
586
648
differ = DiffSymlink(old_tree, new_tree, output_to)
587
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'
589
655
def kind_character(self):
590
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))
593
675
def _read_tree_state(self, path, work_tree):
594
676
"""See InventoryEntry._read_tree_state."""
595
self.symlink_target = work_tree.get_symlink_target(
596
work_tree.id2path(self.file_id), self.file_id)
677
self.symlink_target = work_tree.get_symlink_target(self.file_id)
598
679
def _forget_tree_state(self):
599
680
self.symlink_target = None
658
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)
661
753
def has_filename(self, filename):
662
754
return bool(self.path2id(filename))
665
757
"""Return as a string the path to file_id.
667
759
>>> i = Inventory()
668
>>> e = i.add(InventoryDirectory(b'src-id', 'src', ROOT_ID))
669
>>> e = i.add(InventoryFile(b'foo-id', 'foo.c', parent_id='src-id'))
670
>>> 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')
673
765
:raises NoSuchId: If file_id is not present in the inventory.
690
782
from_dir = self.root
691
783
yield '', self.root
692
elif isinstance(from_dir, bytes):
693
from_dir = self.get_entry(from_dir)
784
elif isinstance(from_dir, basestring):
785
from_dir = self[from_dir]
695
787
# unrolling the recursive called changed the time from
696
788
# 440ms/663ms (inline/total) to 116ms/116ms
697
children = sorted(viewitems(from_dir.children))
789
children = from_dir.children.items()
698
791
if not recursive:
699
792
for name, ie in children:
702
children = deque(children)
795
children = collections.deque(children)
703
796
stack = [(u'', children)]
705
798
from_dir_relpath, children = stack[-1]
728
822
# if we finished all children, pop it off the stack
731
def _preload_cache(self):
732
"""Populate any caches, we are about to access all items.
734
The default implementation does nothing, because CommonInventory doesn't
739
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):
740
827
"""Iterate over the entries in a directory first order.
742
829
This returns all entries for a directory before returning
744
831
lexicographically sorted order, and is a hybrid between
745
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.
747
837
:return: This yields (path, entry) pairs
749
839
if specific_file_ids and not isinstance(specific_file_ids, set):
750
840
specific_file_ids = set(specific_file_ids)
751
841
# TODO? Perhaps this should return the from_dir so that the root is
752
842
# yielded? or maybe an option?
753
if from_dir is None and specific_file_ids is None:
754
# They are iterating from the root, and have not specified any
755
# specific entries to look at. All current callers fully consume the
756
# iterator, so we can safely assume we are accessing all entries
757
self._preload_cache()
758
843
if from_dir is None:
759
844
if self.root is None:
761
846
# Optimize a common case
762
if (specific_file_ids is not None
763
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):
764
849
file_id = list(specific_file_ids)[0]
765
if file_id is not None:
767
path = self.id2path(file_id)
768
except errors.NoSuchId:
771
yield path, self.get_entry(file_id)
851
yield self.id2path(file_id), self[file_id]
773
853
from_dir = self.root
774
if (specific_file_ids is None
775
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):
776
856
yield u'', self.root
777
elif isinstance(from_dir, bytes):
778
from_dir = self.get_entry(from_dir)
780
raise TypeError(from_dir)
857
elif isinstance(from_dir, basestring):
858
from_dir = self[from_dir]
782
860
if specific_file_ids is not None:
783
861
# TODO: jam 20070302 This could really be done as a loop rather
784
862
# than a bunch of recursive calls.
788
865
def add_ancestors(file_id):
789
if not byid.has_id(file_id):
866
if file_id not in byid:
791
parent_id = byid.get_entry(file_id).parent_id
868
parent_id = byid[file_id].parent_id
792
869
if parent_id is None:
794
871
if parent_id not in parents:
804
881
cur_relpath, cur_dir = stack.pop()
807
for child_name, child_ie in sorted(viewitems(cur_dir.children)):
884
for child_name, child_ie in sorted(cur_dir.children.iteritems()):
809
886
child_relpath = cur_relpath + child_name
811
if (specific_file_ids is None
812
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)):
813
891
yield child_relpath, child_ie
815
893
if child_ie.kind == 'directory':
816
894
if parents is None or child_ie.file_id in parents:
817
child_dirs.append((child_relpath + '/', child_ie))
895
child_dirs.append((child_relpath+'/', child_ie))
818
896
stack.extend(reversed(child_dirs))
820
898
def _make_delta(self, old):
821
899
"""Make an inventory delta from two inventories."""
822
old_ids = set(old.iter_all_ids())
823
new_ids = set(self.iter_all_ids())
824
902
adds = new_ids - old_ids
825
903
deletes = old_ids - new_ids
826
904
common = old_ids.intersection(new_ids)
828
906
for file_id in deletes:
829
907
delta.append((old.id2path(file_id), None, file_id, None))
830
908
for file_id in adds:
831
delta.append((None, self.id2path(file_id),
832
file_id, self.get_entry(file_id)))
909
delta.append((None, self.id2path(file_id), file_id, self[file_id]))
833
910
for file_id in common:
834
if old.get_entry(file_id) != self.get_entry(file_id):
911
if old[file_id] != self[file_id]:
835
912
delta.append((old.id2path(file_id), self.id2path(file_id),
836
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)
839
924
def make_entry(self, kind, name, parent_id, file_id=None):
840
"""Simple thunk to breezy.bzr.inventory.make_entry."""
925
"""Simple thunk to bzrlib.inventory.make_entry."""
841
926
return make_entry(kind, name, parent_id, file_id)
843
928
def entries(self):
846
931
This may be faster than iter_entries.
850
934
def descend(dir_ie, dir_path):
851
kids = sorted(viewitems(dir_ie.children))
935
kids = dir_ie.children.items()
852
937
for name, ie in kids:
853
938
child_path = osutils.pathjoin(dir_path, name)
854
939
accum.append((child_path, ie))
855
940
if ie.kind == 'directory':
856
941
descend(ie, child_path)
858
if self.root is not None:
859
descend(self.root, u'')
943
descend(self.root, u'')
862
def get_entry_by_path_partial(self, relpath):
863
"""Like get_entry_by_path, but return TreeReference objects.
865
:param relpath: Path to resolve, either as string with / as separators,
866
or as list of elements.
867
:return: tuple with ie, resolved elements and elements left to resolve
946
def directories(self):
947
"""Return (path, entry) pairs for all directories, including the root.
869
if isinstance(relpath, (str, text_type)):
870
names = osutils.splitpath(relpath)
876
except errors.NoSuchId:
877
# root doesn't exist yet so nothing else can
878
return None, None, None
880
return None, None, None
881
for i, f in enumerate(names):
883
children = getattr(parent, 'children', None)
885
return None, None, None
887
if cie.kind == 'tree-reference':
888
return cie, names[:i + 1], names[i + 1:]
892
return None, None, None
893
return parent, names, []
895
def get_entry_by_path(self, relpath):
896
"""Return an inventory entry by path.
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.
898
965
:param relpath: may be either a list of path components, or a single
899
966
string, in which case it is automatically split.
926
993
# or raise an error?
930
def path2id(self, relpath):
931
"""Walk down through directories to return entry of last component.
933
:param relpath: may be either a list of path components, or a single
934
string, in which case it is automatically split.
936
This returns the entry of the last component in the path,
937
which may be either a file or a directory.
939
Returns None IFF the path is not found.
941
ie = self.get_entry_by_path(relpath)
996
return parent.file_id
946
998
def filter(self, specific_fileids):
947
999
"""Get an inventory view filtered against a set of file-ids.
961
1013
entries = self.iter_entries()
962
1014
if self.root is None:
963
1015
return Inventory(root_id=None)
964
other = Inventory(next(entries)[1].file_id)
1016
other = Inventory(entries.next()[1].file_id)
965
1017
other.root.revision = self.root.revision
966
1018
other.revision_id = self.revision_id
967
1019
directories_to_expand = set()
968
1020
for path, entry in entries:
969
1021
file_id = entry.file_id
970
if (file_id in specific_fileids or
971
entry.parent_id in directories_to_expand):
1022
if (file_id in specific_fileids
1023
or entry.parent_id in directories_to_expand):
972
1024
if entry.kind == 'directory':
973
1025
directories_to_expand.add(file_id)
974
1026
elif file_id not in interesting_parents:
999
1051
returned quickly.
1001
1053
>>> inv = Inventory()
1002
>>> inv.add(InventoryFile(b'123-123', 'hello.c', ROOT_ID))
1054
>>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
1003
1055
InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
1004
>>> inv.get_entry(b'123-123').name
1056
>>> inv['123-123'].name
1007
1059
Id's may be looked up from paths:
1009
1061
>>> inv.path2id('hello.c')
1011
>>> inv.has_id(b'123-123')
1063
>>> '123-123' in inv
1014
1066
There are iterators over the contents:
1056
1108
applied the final inventory must be internally consistent, but it
1057
1109
is ok to supply changes which, if only half-applied would have an
1058
1110
invalid result - such as supplying two changes which rename two
1059
files, 'A' and 'B' with each other : [('A', 'B', b'A-id', a_entry),
1060
('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)].
1062
1114
Each change is a tuple, of the form (old_path, new_path, file_id,
1102
1154
# after their children, which means that everything we examine has no
1103
1155
# modified children remaining by the time we examine it.
1104
1156
for old_path, file_id in sorted(((op, f) for op, np, f, e in delta
1105
if op is not None), reverse=True):
1157
if op is not None), reverse=True):
1106
1158
# Preserve unaltered children of file_id for later reinsertion.
1107
file_id_children = getattr(self.get_entry(file_id), 'children', {})
1159
file_id_children = getattr(self[file_id], 'children', {})
1108
1160
if len(file_id_children):
1109
1161
children[file_id] = file_id_children
1110
1162
if self.id2path(file_id) != old_path:
1111
1163
raise errors.InconsistentDelta(old_path, file_id,
1112
"Entry was at wrong other path %r." % self.id2path(file_id))
1164
"Entry was at wrong other path %r." % self.id2path(file_id))
1113
1165
# Remove file_id and the unaltered children. If file_id is not
1114
1166
# being deleted it will be reinserted back later.
1115
1167
self.remove_recursive_id(file_id)
1119
1171
# the resulting inventory were also modified, are inserted after their
1121
1173
for new_path, f, new_entry in sorted((np, f, e) for op, np, f, e in
1122
delta if np is not None):
1174
delta if np is not None):
1123
1175
if new_entry.kind == 'directory':
1124
1176
# Pop the child which to allow detection of children whose
1125
1177
# parents were deleted and which were not reattached to a new
1127
1179
replacement = InventoryDirectory(new_entry.file_id,
1128
new_entry.name, new_entry.parent_id)
1180
new_entry.name, new_entry.parent_id)
1129
1181
replacement.revision = new_entry.revision
1130
1182
replacement.children = children.pop(replacement.file_id, {})
1131
1183
new_entry = replacement
1133
1185
self.add(new_entry)
1134
1186
except errors.DuplicateFileId:
1135
1187
raise errors.InconsistentDelta(new_path, new_entry.file_id,
1136
"New id is already present in target.")
1188
"New id is already present in target.")
1137
1189
except AttributeError:
1138
1190
raise errors.InconsistentDelta(new_path, new_entry.file_id,
1139
"Parent is not a directory.")
1191
"Parent is not a directory.")
1140
1192
if self.id2path(new_entry.file_id) != new_path:
1141
1193
raise errors.InconsistentDelta(new_path, new_entry.file_id,
1142
"New path is not consistent with parent path.")
1194
"New path is not consistent with parent path.")
1143
1195
if len(children):
1144
1196
# Get the parent id that was deleted
1145
1197
parent_id, children = children.popitem()
1146
1198
raise errors.InconsistentDelta("<deleted>", parent_id,
1147
"The file id was deleted but its children were not deleted.")
1199
"The file id was deleted but its children were not deleted.")
1149
1201
def create_by_apply_delta(self, inventory_delta, new_revision_id,
1150
1202
propagate_caches=False):
1171
1223
other.add(entry.copy())
1174
def iter_all_ids(self):
1226
def _get_mutable_inventory(self):
1227
"""See CommonInventory._get_mutable_inventory."""
1228
return copy.deepcopy(self)
1175
1231
"""Iterate over all file-ids."""
1176
1232
return iter(self._byid)
1178
1234
def iter_just_entries(self):
1179
1235
"""Iterate over all entries.
1181
1237
Unlike iter_entries(), just the entries are returned (not (path, ie))
1182
1238
and the order of entries is undefined.
1184
1240
XXX: We may not want to merge this into bzr.dev.
1186
1242
if self.root is None:
1188
return iter(viewvalues(self._byid))
1244
for _, ie in self._byid.iteritems():
1190
1247
def __len__(self):
1191
1248
"""Returns number of entries."""
1192
1249
return len(self._byid)
1194
def get_entry(self, file_id):
1251
def __getitem__(self, file_id):
1195
1252
"""Return the entry for given file_id.
1197
1254
>>> inv = Inventory()
1198
>>> inv.add(InventoryFile(b'123123', 'hello.c', ROOT_ID))
1255
>>> inv.add(InventoryFile('123123', 'hello.c', ROOT_ID))
1199
1256
InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
1200
>>> inv.get_entry(b'123123').name
1257
>>> inv['123123'].name
1203
if not isinstance(file_id, bytes):
1204
raise TypeError(file_id)
1206
1261
return self._byid[file_id]
1207
1262
except KeyError:
1212
1267
return self._byid[file_id].kind
1214
1269
def get_child(self, parent_id, filename):
1215
return self.get_entry(parent_id).children.get(filename)
1270
return self[parent_id].children.get(filename)
1217
1272
def _add_child(self, entry):
1218
1273
"""Add an entry to the inventory, without adding it to its parent"""
1219
1274
if entry.file_id in self._byid:
1220
raise errors.BzrError(
1221
"inventory already contains entry with id {%s}" %
1275
raise BzrError("inventory already contains entry with id {%s}" %
1223
1277
self._byid[entry.file_id] = entry
1224
children = getattr(entry, 'children', {})
1225
if children is not None:
1226
for child in viewvalues(children):
1227
self._add_child(child)
1278
for child in getattr(entry, 'children', {}).itervalues():
1279
self._add_child(child)
1230
1282
def add(self, entry):
1231
1283
"""Add entry to inventory.
1285
To add a file to a branch ready to be committed, use Branch.add,
1235
1290
if entry.file_id in self._byid:
1274
1329
ie = make_entry(kind, parts[-1], parent_id, file_id)
1275
1330
return self.add(ie)
1277
def delete(self, file_id):
1332
def __delitem__(self, file_id):
1278
1333
"""Remove entry by id.
1280
1335
>>> inv = Inventory()
1281
>>> inv.add(InventoryFile(b'123', 'foo.c', ROOT_ID))
1336
>>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
1282
1337
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
1283
>>> inv.has_id(b'123')
1285
>>> inv.delete(b'123')
1286
>>> inv.has_id(b'123')
1289
ie = self.get_entry(file_id)
1290
1345
del self._byid[file_id]
1291
1346
if ie.parent_id is not None:
1292
del self.get_entry(ie.parent_id).children[ie.name]
1347
del self[ie.parent_id].children[ie.name]
1294
1349
def __eq__(self, other):
1295
1350
"""Compare two sets by comparing their contents.
1298
1353
>>> i2 = Inventory()
1301
>>> i1.add(InventoryFile(b'123', 'foo', ROOT_ID))
1356
>>> i1.add(InventoryFile('123', 'foo', ROOT_ID))
1302
1357
InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
1305
>>> i2.add(InventoryFile(b'123', 'foo', ROOT_ID))
1360
>>> i2.add(InventoryFile('123', 'foo', ROOT_ID))
1306
1361
InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
1334
1389
def _make_delta(self, old):
1335
1390
"""Make an inventory delta from two inventories."""
1336
old_getter = old.get_entry
1337
new_getter = self.get_entry
1338
old_ids = set(old.iter_all_ids())
1339
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)
1340
1395
adds = new_ids - old_ids
1341
1396
deletes = old_ids - new_ids
1342
1397
if not adds and not deletes:
1347
1402
for file_id in deletes:
1348
1403
delta.append((old.id2path(file_id), None, file_id, None))
1349
1404
for file_id in adds:
1350
delta.append((None, self.id2path(file_id),
1351
file_id, self.get_entry(file_id)))
1405
delta.append((None, self.id2path(file_id), file_id, self[file_id]))
1352
1406
for file_id in common:
1353
new_ie = new_getter(file_id)
1354
old_ie = old_getter(file_id)
1407
new_ie = new_getter[file_id]
1408
old_ie = old_getter[file_id]
1355
1409
# If xml_serializer returns the cached InventoryEntries (rather
1356
1410
# than always doing .copy()), inlining the 'is' check saves 2.7M
1357
1411
# calls to __eq__. Under lsprof this saves 20s => 6s.
1393
1447
new_name = ensure_normalized_name(new_name)
1394
1448
if not is_valid_name(new_name):
1395
raise errors.BzrError("not an acceptable filename: %r" % new_name)
1449
raise BzrError("not an acceptable filename: %r" % new_name)
1397
1451
new_parent = self._byid[new_parent_id]
1398
1452
if new_name in new_parent.children:
1399
raise errors.BzrError("%r already exists in %r" %
1400
(new_name, self.id2path(new_parent_id)))
1453
raise BzrError("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
1402
1455
new_parent_idpath = self.get_idpath(new_parent_id)
1403
1456
if file_id in new_parent_idpath:
1404
raise errors.BzrError(
1405
"cannot move directory %r into a subdirectory of itself, %r"
1406
% (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)))
1408
1460
file_ie = self._byid[file_id]
1409
1461
old_parent = self._byid[file_ie.parent_id]
1480
1531
if entry.parent_id is not None:
1481
1532
parent_str = entry.parent_id
1484
1535
name_str = entry.name.encode("utf8")
1485
1536
if entry.kind == 'file':
1486
1537
if entry.executable:
1490
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" % (
1491
1542
entry.file_id, parent_str, name_str, entry.revision,
1492
1543
entry.text_sha1, entry.text_size, exec_str)
1493
1544
elif entry.kind == 'directory':
1494
return b"dir: %s\n%s\n%s\n%s" % (
1545
return "dir: %s\n%s\n%s\n%s" % (
1495
1546
entry.file_id, parent_str, name_str, entry.revision)
1496
1547
elif entry.kind == 'symlink':
1497
return b"symlink: %s\n%s\n%s\n%s\n%s" % (
1548
return "symlink: %s\n%s\n%s\n%s\n%s" % (
1498
1549
entry.file_id, parent_str, name_str, entry.revision,
1499
1550
entry.symlink_target.encode("utf8"))
1500
1551
elif entry.kind == 'tree-reference':
1501
return b"tree: %s\n%s\n%s\n%s\n%s" % (
1552
return "tree: %s\n%s\n%s\n%s\n%s" % (
1502
1553
entry.file_id, parent_str, name_str, entry.revision,
1503
1554
entry.reference_revision)
1546
1597
remaining_parents = interesting.difference(file_ids)
1547
1598
# When we hit the TREE_ROOT, we'll get an interesting parent of None,
1548
1599
# but we don't actually want to recurse into that
1549
interesting.add(None) # this will auto-filter it in the loop
1550
remaining_parents.discard(None)
1600
interesting.add(None) # this will auto-filter it in the loop
1601
remaining_parents.discard(None)
1551
1602
while remaining_parents:
1552
1603
next_parents = set()
1553
1604
for entry in self._getitems(remaining_parents):
1554
1605
next_parents.add(entry.parent_id)
1555
children_of_parent_id.setdefault(entry.parent_id, set()
1556
).add(entry.file_id)
1606
children_of_parent_id.setdefault(entry.parent_id, []
1607
).append(entry.file_id)
1557
1608
# Remove any search tips we've already processed
1558
1609
remaining_parents = next_parents.difference(interesting)
1559
1610
interesting.update(remaining_parents)
1566
1617
keys = [StaticTuple(f,).intern() for f in directories_to_expand]
1567
1618
directories_to_expand = set()
1568
1619
items = self.parent_id_basename_to_file_id.iteritems(keys)
1569
next_file_ids = {item[1] for item in items}
1620
next_file_ids = set([item[1] for item in items])
1570
1621
next_file_ids = next_file_ids.difference(interesting)
1571
1622
interesting.update(next_file_ids)
1572
1623
for entry in self._getitems(next_file_ids):
1573
1624
if entry.kind == 'directory':
1574
1625
directories_to_expand.add(entry.file_id)
1575
children_of_parent_id.setdefault(entry.parent_id, set()
1576
).add(entry.file_id)
1626
children_of_parent_id.setdefault(entry.parent_id, []
1627
).append(entry.file_id)
1577
1628
return interesting, children_of_parent_id
1579
1630
def filter(self, specific_fileids):
1601
1652
# parent_to_children with at least the tree root.)
1603
1654
cache = self._fileid_to_entry_cache
1604
remaining_children = deque(
1605
parent_to_children[self.root_id])
1656
remaining_children = collections.deque(parent_to_children[self.root_id])
1658
import pdb; pdb.set_trace()
1606
1660
while remaining_children:
1607
1661
file_id = remaining_children.popleft()
1608
1662
ie = cache[file_id]
1609
1663
if ie.kind == 'directory':
1610
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
1611
1665
# TODO: depending on the uses of 'other' we should probably alwyas
1612
1666
# '.copy()' to prevent someone from mutating other and
1613
1667
# invaliding our internal cache
1620
def _bytes_to_utf8name_key(data):
1621
"""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."""
1622
1676
# We don't normally care about name, except for times when we want
1623
1677
# to filter out empty names because of non rich-root...
1624
sections = data.split(b'\n')
1625
kind, file_id = sections[0].split(b': ')
1626
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]))
1628
1682
def _bytes_to_entry(self, bytes):
1629
1683
"""Deserialise a serialised entry."""
1630
sections = bytes.split(b'\n')
1631
if sections[0].startswith(b"file: "):
1684
sections = bytes.split('\n')
1685
if sections[0].startswith("file: "):
1632
1686
result = InventoryFile(sections[0][6:],
1633
sections[2].decode('utf8'),
1687
sections[2].decode('utf8'),
1635
1689
result.text_sha1 = sections[4]
1636
1690
result.text_size = int(sections[5])
1637
result.executable = sections[6] == b"Y"
1638
elif sections[0].startswith(b"dir: "):
1691
result.executable = sections[6] == "Y"
1692
elif sections[0].startswith("dir: "):
1639
1693
result = CHKInventoryDirectory(sections[0][5:],
1640
sections[2].decode('utf8'),
1642
elif sections[0].startswith(b"symlink: "):
1694
sections[2].decode('utf8'),
1696
elif sections[0].startswith("symlink: "):
1643
1697
result = InventoryLink(sections[0][9:],
1644
sections[2].decode('utf8'),
1698
sections[2].decode('utf8'),
1646
1700
result.symlink_target = sections[4].decode('utf8')
1647
elif sections[0].startswith(b"tree: "):
1701
elif sections[0].startswith("tree: "):
1648
1702
result = TreeReference(sections[0][6:],
1649
sections[2].decode('utf8'),
1703
sections[2].decode('utf8'),
1651
1705
result.reference_revision = sections[4]
1653
1707
raise ValueError("Not a serialised entry %r" % bytes)
1654
result.file_id = bytesintern(result.file_id)
1655
result.revision = bytesintern(sections[3])
1656
if result.parent_id == b'':
1708
result.file_id = intern(result.file_id)
1709
result.revision = intern(sections[3])
1710
if result.parent_id == '':
1657
1711
result.parent_id = None
1658
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())
1661
1723
def create_by_apply_delta(self, inventory_delta, new_revision_id,
1662
propagate_caches=False):
1724
propagate_caches=False):
1663
1725
"""Create a new CHKInventory by applying inventory_delta to this one.
1665
1727
See the inventory developers documentation for the theory behind
1791
1852
new_key, [None, None])[1] = new_value
1792
1853
# validate that deletes are complete.
1793
1854
for file_id in deletes:
1794
entry = self.get_entry(file_id)
1855
entry = self[file_id]
1795
1856
if entry.kind != 'directory':
1797
1858
# This loop could potentially be better by using the id_basename
1798
1859
# map to just get the child file ids.
1799
for child in viewvalues(entry.children):
1860
for child in entry.children.values():
1800
1861
if child.file_id not in altered:
1801
1862
raise errors.InconsistentDelta(self.id2path(child.file_id),
1802
child.file_id, "Child not deleted or reparented when "
1863
child.file_id, "Child not deleted or reparented when "
1804
1865
result.id_to_entry.apply_delta(id_to_entry_delta)
1805
1866
if parent_id_basename_delta:
1806
1867
# Transform the parent_id_basename delta data into a linear delta
1817
1878
parents.discard(('', None))
1818
1879
for parent_path, parent in parents:
1820
if result.get_entry(parent).kind != 'directory':
1881
if result[parent].kind != 'directory':
1821
1882
raise errors.InconsistentDelta(result.id2path(parent), parent,
1822
'Not a directory, but given children')
1883
'Not a directory, but given children')
1823
1884
except errors.NoSuchId:
1824
1885
raise errors.InconsistentDelta("<unknown>", parent,
1825
"Parent is not present in resulting inventory.")
1886
"Parent is not present in resulting inventory.")
1826
1887
if result.path2id(parent_path) != parent:
1827
1888
raise errors.InconsistentDelta(parent_path, parent,
1828
"Parent has wrong path %r." % result.path2id(parent_path))
1889
"Parent has wrong path %r." % result.path2id(parent_path))
1832
def deserialise(klass, chk_store, lines, expected_revision_id):
1893
def deserialise(klass, chk_store, bytes, expected_revision_id):
1833
1894
"""Deserialise a CHKInventory.
1835
1896
:param chk_store: A CHK capable VersionedFiles instance.
1839
1900
:return: A CHKInventory
1841
if not lines[-1].endswith(b'\n'):
1842
raise ValueError("last line should have trailing eol\n")
1843
if lines[0] != b'chkinventory:\n':
1902
lines = bytes.split('\n')
1904
raise AssertionError('bytes to deserialize must end with an eol')
1906
if lines[0] != 'chkinventory:':
1844
1907
raise ValueError("not a serialised CHKInventory: %r" % bytes)
1846
allowed_keys = frozenset((b'root_id', b'revision_id',
1847
b'parent_id_basename_to_file_id',
1848
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',
1849
1912
for line in lines[1:]:
1850
key, value = line.rstrip(b'\n').split(b': ', 1)
1913
key, value = line.split(': ', 1)
1851
1914
if key not in allowed_keys:
1852
1915
raise errors.BzrError('Unknown key in inventory: %r\n%r'
1853
1916
% (key, bytes))
1855
1918
raise errors.BzrError('Duplicate key in inventory: %r\n%r'
1856
1919
% (key, bytes))
1857
1920
info[key] = value
1858
revision_id = bytesintern(info[b'revision_id'])
1859
root_id = bytesintern(info[b'root_id'])
1860
search_key_name = bytesintern(info.get(b'search_key_name', b'plain'))
1861
parent_id_basename_to_file_id = bytesintern(info.get(
1862
b'parent_id_basename_to_file_id', None))
1863
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:'):
1864
1927
raise ValueError('parent_id_basename_to_file_id should be a sha1'
1865
1928
' key not %r' % (parent_id_basename_to_file_id,))
1866
id_to_entry = info[b'id_to_entry']
1867
if not id_to_entry.startswith(b'sha1:'):
1929
id_to_entry = info['id_to_entry']
1930
if not id_to_entry.startswith('sha1:'):
1868
1931
raise ValueError('id_to_entry should be a sha1'
1869
1932
' key not %r' % (id_to_entry,))
1885
1948
search_key_func=search_key_func)
1886
1949
if (result.revision_id,) != expected_revision_id:
1887
1950
raise ValueError("Mismatched revision id and expected: %r, %r" %
1888
(result.revision_id, expected_revision_id))
1951
(result.revision_id, expected_revision_id))
1892
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'):
1893
1956
"""Create a CHKInventory from an existing inventory.
1895
1958
The content of inventory is copied into the chk_store, and a
1915
1978
parent_id_basename_dict[p_id_key] = entry.file_id
1917
1980
result._populate_from_dicts(chk_store, id_to_entry_dict,
1918
parent_id_basename_dict, maximum_size=maximum_size)
1981
parent_id_basename_dict, maximum_size=maximum_size)
1921
1984
def _populate_from_dicts(self, chk_store, id_to_entry_dict,
1922
1985
parent_id_basename_dict, maximum_size):
1923
search_key_func = chk_map.search_key_registry.get(
1924
self._search_key_name)
1986
search_key_func = chk_map.search_key_registry.get(self._search_key_name)
1925
1987
root_key = chk_map.CHKMap.from_dict(chk_store, id_to_entry_dict,
1926
maximum_size=maximum_size, key_width=1,
1927
search_key_func=search_key_func)
1988
maximum_size=maximum_size, key_width=1,
1989
search_key_func=search_key_func)
1928
1990
self.id_to_entry = chk_map.CHKMap(chk_store, root_key,
1929
1991
search_key_func)
1930
1992
root_key = chk_map.CHKMap.from_dict(chk_store,
1931
parent_id_basename_dict,
1932
maximum_size=maximum_size, key_width=2,
1933
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)
1934
1996
self.parent_id_basename_to_file_id = chk_map.CHKMap(chk_store,
1935
root_key, search_key_func)
1997
root_key, search_key_func)
1937
1999
def _parent_id_basename_key(self, entry):
1938
2000
"""Create a key for a entry in a parent_id_basename_to_file_id index."""
1939
2001
if entry.parent_id is not None:
1940
2002
parent_id = entry.parent_id
1943
2005
return StaticTuple(parent_id, entry.name.encode('utf8')).intern()
1945
def get_entry(self, file_id):
2007
def __getitem__(self, file_id):
1946
2008
"""map a single file_id -> InventoryEntry."""
1947
2009
if file_id is None:
1948
2010
raise errors.NoSuchId(self, file_id)
1953
2015
return self._bytes_to_entry(
1954
next(self.id_to_entry.iteritems([StaticTuple(file_id,)]))[1])
2016
self.id_to_entry.iteritems([StaticTuple(file_id,)]).next()[1])
1955
2017
except StopIteration:
1956
2018
# really we're passing an inventory, not a tree...
1957
2019
raise errors.NoSuchId(self, file_id)
1959
2021
def _getitems(self, file_ids):
1960
"""Similar to get_entry, but lets you query for multiple.
2022
"""Similar to __getitem__, but lets you query for multiple.
1962
2024
The returned order is undefined. And currently if an item doesn't
1963
2025
exist, it isn't included in the output.
1991
2053
"""Yield the parents of file_id up to the root."""
1992
2054
while file_id is not None:
1994
ie = self.get_entry(file_id)
1995
2057
except KeyError:
1996
2058
raise errors.NoSuchId(tree=self, file_id=file_id)
1998
2060
file_id = ie.parent_id
2000
def iter_all_ids(self):
2001
2063
"""Iterate over all file-ids."""
2002
2064
for key, _ in self.id_to_entry.iteritems():
2005
2067
def iter_just_entries(self):
2006
2068
"""Iterate over all entries.
2008
2070
Unlike iter_entries(), just the entries are returned (not (path, ie))
2009
2071
and the order of entries is undefined.
2018
2080
self._fileid_to_entry_cache[file_id] = ie
2021
def _preload_cache(self):
2022
"""Make sure all file-ids are in _fileid_to_entry_cache"""
2023
if self._fully_cached:
2024
return # No need to do it again
2025
# The optimal sort order is to use iteritems() directly
2026
cache = self._fileid_to_entry_cache
2027
for key, entry in self.id_to_entry.iteritems():
2029
if file_id not in cache:
2030
ie = self._bytes_to_entry(entry)
2034
last_parent_id = last_parent_ie = None
2035
pid_items = self.parent_id_basename_to_file_id.iteritems()
2036
for key, child_file_id in pid_items:
2037
if key == (b'', b''): # This is the root
2038
if child_file_id != self.root_id:
2039
raise ValueError('Data inconsistency detected.'
2040
' We expected data with key ("","") to match'
2041
' the root id, but %s != %s'
2042
% (child_file_id, self.root_id))
2044
parent_id, basename = key
2045
ie = cache[child_file_id]
2046
if parent_id == last_parent_id:
2047
parent_ie = last_parent_ie
2049
parent_ie = cache[parent_id]
2050
if parent_ie.kind != 'directory':
2051
raise ValueError('Data inconsistency detected.'
2052
' An entry in the parent_id_basename_to_file_id map'
2053
' has parent_id {%s} but the kind of that object'
2054
' is %r not "directory"' % (parent_id, parent_ie.kind))
2055
if parent_ie._children is None:
2056
parent_ie._children = {}
2057
basename = basename.decode('utf-8')
2058
if basename in parent_ie._children:
2059
existing_ie = parent_ie._children[basename]
2060
if existing_ie != ie:
2061
raise ValueError('Data inconsistency detected.'
2062
' Two entries with basename %r were found'
2063
' in the parent entry {%s}'
2064
% (basename, parent_id))
2065
if basename != ie.name:
2066
raise ValueError('Data inconsistency detected.'
2067
' In the parent_id_basename_to_file_id map, file_id'
2068
' {%s} is listed as having basename %r, but in the'
2069
' id_to_entry map it is %r'
2070
% (child_file_id, basename, ie.name))
2071
parent_ie._children[basename] = ie
2072
self._fully_cached = True
2074
2083
def iter_changes(self, basis):
2075
2084
"""Generate a Tree.iter_changes change list between this and basis.
2121
2130
if kind[0] != kind[1]:
2122
2131
changed_content = True
2123
2132
elif kind[0] == 'file':
2124
if (self_entry.text_size != basis_entry.text_size
2125
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):
2126
2135
changed_content = True
2127
2136
elif kind[0] == 'symlink':
2128
2137
if self_entry.symlink_target != basis_entry.symlink_target:
2129
2138
changed_content = True
2130
2139
elif kind[0] == 'tree-reference':
2131
if (self_entry.reference_revision
2132
!= basis_entry.reference_revision):
2140
if (self_entry.reference_revision !=
2141
basis_entry.reference_revision):
2133
2142
changed_content = True
2134
2143
parent = (basis_parent, self_parent)
2135
2144
name = (basis_name, self_name)
2136
2145
executable = (basis_executable, self_executable)
2137
if (not changed_content and
2138
parent[0] == parent[1] and
2139
name[0] == name[1] and
2140
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]):
2141
2150
# Could happen when only the revision changed for a directory
2142
2151
# for instance.
2145
file_id, (path_in_source, path_in_target), changed_content,
2153
yield (file_id, (path_in_source, path_in_target), changed_content,
2146
2154
versioned, parent, name, kind, executable)
2148
2156
def __len__(self):
2152
2160
def _make_delta(self, old):
2153
2161
"""Make an inventory delta from two inventories."""
2154
if not isinstance(old, CHKInventory):
2162
if type(old) != CHKInventory:
2155
2163
return CommonInventory._make_delta(self, old)
2157
2165
for key, old_value, self_value in \
2158
self.id_to_entry.iter_changes(old.id_to_entry):
2166
self.id_to_entry.iter_changes(old.id_to_entry):
2159
2167
file_id = key[0]
2160
2168
if old_value is not None:
2161
2169
old_path = old.id2path(file_id)
2174
2182
def path2id(self, relpath):
2175
2183
"""See CommonInventory.path2id()."""
2176
2184
# TODO: perhaps support negative hits?
2177
if isinstance(relpath, (str, text_type)):
2178
names = osutils.splitpath(relpath)
2183
relpath = osutils.pathjoin(*relpath)
2184
2185
result = self._path_to_fileid_cache.get(relpath, None)
2185
2186
if result is not None:
2188
if isinstance(relpath, basestring):
2189
names = osutils.splitpath(relpath)
2187
2192
current_id = self.root_id
2188
2193
if current_id is None:
2214
2219
def to_lines(self):
2215
2220
"""Serialise the inventory to lines."""
2216
lines = [b"chkinventory:\n"]
2217
if self._search_key_name != b'plain':
2221
lines = ["chkinventory:\n"]
2222
if self._search_key_name != 'plain':
2218
2223
# custom ordering grouping things that don't change together
2219
lines.append(b'search_key_name: %s\n' % (
2220
self._search_key_name))
2221
lines.append(b"root_id: %s\n" % self.root_id)
2222
lines.append(b'parent_id_basename_to_file_id: %s\n' %
2223
(self.parent_id_basename_to_file_id.key()[0],))
2224
lines.append(b"revision_id: %s\n" % self.revision_id)
2225
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],))
2227
lines.append(b"revision_id: %s\n" % self.revision_id)
2228
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)
2229
2233
if self.parent_id_basename_to_file_id is not None:
2230
lines.append(b'parent_id_basename_to_file_id: %s\n' %
2231
(self.parent_id_basename_to_file_id.key()[0],))
2232
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],))
2236
2240
def root(self):
2237
2241
"""Get the root entry."""
2238
return self.get_entry(self.root_id)
2242
return self[self.root_id]
2241
2245
class CHKInventoryDirectory(InventoryDirectory):
2242
2246
"""A directory in an inventory."""
2244
__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',
2246
2253
def __init__(self, file_id, name, parent_id, chk_inventory):
2247
2254
# Don't call InventoryDirectory.__init__ - it isn't right for this
2249
2256
InventoryEntry.__init__(self, file_id, name, parent_id)
2250
2257
self._children = None
2258
self.kind = 'directory'
2251
2259
self._chk_inventory = chk_inventory
2265
2273
# No longer supported
2266
2274
if self._chk_inventory.parent_id_basename_to_file_id is None:
2267
2275
raise AssertionError("Inventories without"
2268
" parent_id_basename_to_file_id are no longer supported")
2276
" parent_id_basename_to_file_id are no longer supported")
2270
2278
# XXX: Todo - use proxy objects for the children rather than loading
2271
2279
# all when the attribute is referenced.
2272
2280
parent_id_index = self._chk_inventory.parent_id_basename_to_file_id
2273
2281
child_keys = set()
2274
2282
for (parent_id, name_utf8), file_id in parent_id_index.iteritems(
2275
key_filter=[StaticTuple(self.file_id,)]):
2283
key_filter=[StaticTuple(self.file_id,)]):
2276
2284
child_keys.add(StaticTuple(file_id,))
2278
2286
for file_id_key in child_keys:
2434
2443
entry = item[3]
2435
2444
if new_path is None and entry is not None:
2436
2445
raise errors.InconsistentDelta(item[0], item[1],
2437
"Entry with no new_path")
2446
"Entry with no new_path")
2438
2447
if new_path is not None and entry is None:
2439
2448
raise errors.InconsistentDelta(new_path, item[1],
2440
"new_path with no entry")
2449
"new_path with no entry")
2444
def mutable_inventory_from_tree(tree):
2445
"""Create a new inventory that has the same contents as a specified tree.
2447
:param tree: Revision tree to create inventory from
2449
entries = tree.iter_entries_by_dir()
2450
inv = Inventory(None, tree.get_revision_id())
2451
for path, inv_entry in entries:
2452
inv.add(inv_entry.copy())