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])
1655
remaining_children = collections.deque(parent_to_children[self.root_id])
1606
1656
while remaining_children:
1607
1657
file_id = remaining_children.popleft()
1608
1658
ie = cache[file_id]
1609
1659
if ie.kind == 'directory':
1610
ie = ie.copy() # We create a copy to depopulate the .children attribute
1660
ie = ie.copy() # We create a copy to depopulate the .children attribute
1611
1661
# TODO: depending on the uses of 'other' we should probably alwyas
1612
1662
# '.copy()' to prevent someone from mutating other and
1613
1663
# invaliding our internal cache
1620
def _bytes_to_utf8name_key(data):
1621
"""Get the file_id, revision_id key out of data."""
1670
def _bytes_to_utf8name_key(bytes):
1671
"""Get the file_id, revision_id key out of bytes."""
1622
1672
# We don't normally care about name, except for times when we want
1623
1673
# 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]))
1674
sections = bytes.split('\n')
1675
kind, file_id = sections[0].split(': ')
1676
return (sections[2], intern(file_id), intern(sections[3]))
1628
1678
def _bytes_to_entry(self, bytes):
1629
1679
"""Deserialise a serialised entry."""
1630
sections = bytes.split(b'\n')
1631
if sections[0].startswith(b"file: "):
1680
sections = bytes.split('\n')
1681
if sections[0].startswith("file: "):
1632
1682
result = InventoryFile(sections[0][6:],
1633
sections[2].decode('utf8'),
1683
sections[2].decode('utf8'),
1635
1685
result.text_sha1 = sections[4]
1636
1686
result.text_size = int(sections[5])
1637
result.executable = sections[6] == b"Y"
1638
elif sections[0].startswith(b"dir: "):
1687
result.executable = sections[6] == "Y"
1688
elif sections[0].startswith("dir: "):
1639
1689
result = CHKInventoryDirectory(sections[0][5:],
1640
sections[2].decode('utf8'),
1642
elif sections[0].startswith(b"symlink: "):
1690
sections[2].decode('utf8'),
1692
elif sections[0].startswith("symlink: "):
1643
1693
result = InventoryLink(sections[0][9:],
1644
sections[2].decode('utf8'),
1694
sections[2].decode('utf8'),
1646
1696
result.symlink_target = sections[4].decode('utf8')
1647
elif sections[0].startswith(b"tree: "):
1697
elif sections[0].startswith("tree: "):
1648
1698
result = TreeReference(sections[0][6:],
1649
sections[2].decode('utf8'),
1699
sections[2].decode('utf8'),
1651
1701
result.reference_revision = sections[4]
1653
1703
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'':
1704
result.file_id = intern(result.file_id)
1705
result.revision = intern(sections[3])
1706
if result.parent_id == '':
1657
1707
result.parent_id = None
1658
1708
self._fileid_to_entry_cache[result.file_id] = result
1711
def _get_mutable_inventory(self):
1712
"""See CommonInventory._get_mutable_inventory."""
1713
entries = self.iter_entries()
1714
inv = Inventory(None, self.revision_id)
1715
for path, inv_entry in entries:
1716
inv.add(inv_entry.copy())
1661
1719
def create_by_apply_delta(self, inventory_delta, new_revision_id,
1662
propagate_caches=False):
1720
propagate_caches=False):
1663
1721
"""Create a new CHKInventory by applying inventory_delta to this one.
1665
1723
See the inventory developers documentation for the theory behind
1791
1848
new_key, [None, None])[1] = new_value
1792
1849
# validate that deletes are complete.
1793
1850
for file_id in deletes:
1794
entry = self.get_entry(file_id)
1851
entry = self[file_id]
1795
1852
if entry.kind != 'directory':
1797
1854
# This loop could potentially be better by using the id_basename
1798
1855
# map to just get the child file ids.
1799
for child in viewvalues(entry.children):
1856
for child in entry.children.values():
1800
1857
if child.file_id not in altered:
1801
1858
raise errors.InconsistentDelta(self.id2path(child.file_id),
1802
child.file_id, "Child not deleted or reparented when "
1859
child.file_id, "Child not deleted or reparented when "
1804
1861
result.id_to_entry.apply_delta(id_to_entry_delta)
1805
1862
if parent_id_basename_delta:
1806
1863
# Transform the parent_id_basename delta data into a linear delta
1817
1874
parents.discard(('', None))
1818
1875
for parent_path, parent in parents:
1820
if result.get_entry(parent).kind != 'directory':
1877
if result[parent].kind != 'directory':
1821
1878
raise errors.InconsistentDelta(result.id2path(parent), parent,
1822
'Not a directory, but given children')
1879
'Not a directory, but given children')
1823
1880
except errors.NoSuchId:
1824
1881
raise errors.InconsistentDelta("<unknown>", parent,
1825
"Parent is not present in resulting inventory.")
1882
"Parent is not present in resulting inventory.")
1826
1883
if result.path2id(parent_path) != parent:
1827
1884
raise errors.InconsistentDelta(parent_path, parent,
1828
"Parent has wrong path %r." % result.path2id(parent_path))
1885
"Parent has wrong path %r." % result.path2id(parent_path))
1832
def deserialise(klass, chk_store, lines, expected_revision_id):
1889
def deserialise(klass, chk_store, bytes, expected_revision_id):
1833
1890
"""Deserialise a CHKInventory.
1835
1892
:param chk_store: A CHK capable VersionedFiles instance.
1839
1896
: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':
1898
lines = bytes.split('\n')
1900
raise AssertionError('bytes to deserialize must end with an eol')
1902
if lines[0] != 'chkinventory:':
1844
1903
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'))
1905
allowed_keys = frozenset(['root_id', 'revision_id', 'search_key_name',
1906
'parent_id_basename_to_file_id',
1849
1908
for line in lines[1:]:
1850
key, value = line.rstrip(b'\n').split(b': ', 1)
1909
key, value = line.split(': ', 1)
1851
1910
if key not in allowed_keys:
1852
1911
raise errors.BzrError('Unknown key in inventory: %r\n%r'
1853
1912
% (key, bytes))
1855
1914
raise errors.BzrError('Duplicate key in inventory: %r\n%r'
1856
1915
% (key, bytes))
1857
1916
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:'):
1917
revision_id = intern(info['revision_id'])
1918
root_id = intern(info['root_id'])
1919
search_key_name = intern(info.get('search_key_name', 'plain'))
1920
parent_id_basename_to_file_id = intern(info.get(
1921
'parent_id_basename_to_file_id', None))
1922
if not parent_id_basename_to_file_id.startswith('sha1:'):
1864
1923
raise ValueError('parent_id_basename_to_file_id should be a sha1'
1865
1924
' 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:'):
1925
id_to_entry = info['id_to_entry']
1926
if not id_to_entry.startswith('sha1:'):
1868
1927
raise ValueError('id_to_entry should be a sha1'
1869
1928
' key not %r' % (id_to_entry,))
1885
1944
search_key_func=search_key_func)
1886
1945
if (result.revision_id,) != expected_revision_id:
1887
1946
raise ValueError("Mismatched revision id and expected: %r, %r" %
1888
(result.revision_id, expected_revision_id))
1947
(result.revision_id, expected_revision_id))
1892
def from_inventory(klass, chk_store, inventory, maximum_size=0, search_key_name=b'plain'):
1951
def from_inventory(klass, chk_store, inventory, maximum_size=0, search_key_name='plain'):
1893
1952
"""Create a CHKInventory from an existing inventory.
1895
1954
The content of inventory is copied into the chk_store, and a
1915
1974
parent_id_basename_dict[p_id_key] = entry.file_id
1917
1976
result._populate_from_dicts(chk_store, id_to_entry_dict,
1918
parent_id_basename_dict, maximum_size=maximum_size)
1977
parent_id_basename_dict, maximum_size=maximum_size)
1921
1980
def _populate_from_dicts(self, chk_store, id_to_entry_dict,
1922
1981
parent_id_basename_dict, maximum_size):
1923
search_key_func = chk_map.search_key_registry.get(
1924
self._search_key_name)
1982
search_key_func = chk_map.search_key_registry.get(self._search_key_name)
1925
1983
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)
1984
maximum_size=maximum_size, key_width=1,
1985
search_key_func=search_key_func)
1928
1986
self.id_to_entry = chk_map.CHKMap(chk_store, root_key,
1929
1987
search_key_func)
1930
1988
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)
1989
parent_id_basename_dict,
1990
maximum_size=maximum_size, key_width=2,
1991
search_key_func=search_key_func)
1934
1992
self.parent_id_basename_to_file_id = chk_map.CHKMap(chk_store,
1935
root_key, search_key_func)
1993
root_key, search_key_func)
1937
1995
def _parent_id_basename_key(self, entry):
1938
1996
"""Create a key for a entry in a parent_id_basename_to_file_id index."""
1939
1997
if entry.parent_id is not None:
1940
1998
parent_id = entry.parent_id
1943
2001
return StaticTuple(parent_id, entry.name.encode('utf8')).intern()
1945
def get_entry(self, file_id):
2003
def __getitem__(self, file_id):
1946
2004
"""map a single file_id -> InventoryEntry."""
1947
2005
if file_id is None:
1948
2006
raise errors.NoSuchId(self, file_id)
1953
2011
return self._bytes_to_entry(
1954
next(self.id_to_entry.iteritems([StaticTuple(file_id,)]))[1])
2012
self.id_to_entry.iteritems([StaticTuple(file_id,)]).next()[1])
1955
2013
except StopIteration:
1956
2014
# really we're passing an inventory, not a tree...
1957
2015
raise errors.NoSuchId(self, file_id)
1959
2017
def _getitems(self, file_ids):
1960
"""Similar to get_entry, but lets you query for multiple.
2018
"""Similar to __getitem__, but lets you query for multiple.
1962
2020
The returned order is undefined. And currently if an item doesn't
1963
2021
exist, it isn't included in the output.
1991
2049
"""Yield the parents of file_id up to the root."""
1992
2050
while file_id is not None:
1994
ie = self.get_entry(file_id)
1995
2053
except KeyError:
1996
2054
raise errors.NoSuchId(tree=self, file_id=file_id)
1998
2056
file_id = ie.parent_id
2000
def iter_all_ids(self):
2001
2059
"""Iterate over all file-ids."""
2002
2060
for key, _ in self.id_to_entry.iteritems():
2005
2063
def iter_just_entries(self):
2006
2064
"""Iterate over all entries.
2008
2066
Unlike iter_entries(), just the entries are returned (not (path, ie))
2009
2067
and the order of entries is undefined.
2018
2076
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
2079
def iter_changes(self, basis):
2075
2080
"""Generate a Tree.iter_changes change list between this and basis.
2121
2126
if kind[0] != kind[1]:
2122
2127
changed_content = True
2123
2128
elif kind[0] == 'file':
2124
if (self_entry.text_size != basis_entry.text_size
2125
or self_entry.text_sha1 != basis_entry.text_sha1):
2129
if (self_entry.text_size != basis_entry.text_size or
2130
self_entry.text_sha1 != basis_entry.text_sha1):
2126
2131
changed_content = True
2127
2132
elif kind[0] == 'symlink':
2128
2133
if self_entry.symlink_target != basis_entry.symlink_target:
2129
2134
changed_content = True
2130
2135
elif kind[0] == 'tree-reference':
2131
if (self_entry.reference_revision
2132
!= basis_entry.reference_revision):
2136
if (self_entry.reference_revision !=
2137
basis_entry.reference_revision):
2133
2138
changed_content = True
2134
2139
parent = (basis_parent, self_parent)
2135
2140
name = (basis_name, self_name)
2136
2141
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]):
2142
if (not changed_content
2143
and parent[0] == parent[1]
2144
and name[0] == name[1]
2145
and executable[0] == executable[1]):
2141
2146
# Could happen when only the revision changed for a directory
2142
2147
# for instance.
2145
file_id, (path_in_source, path_in_target), changed_content,
2149
yield (file_id, (path_in_source, path_in_target), changed_content,
2146
2150
versioned, parent, name, kind, executable)
2148
2152
def __len__(self):
2152
2156
def _make_delta(self, old):
2153
2157
"""Make an inventory delta from two inventories."""
2154
if not isinstance(old, CHKInventory):
2158
if type(old) != CHKInventory:
2155
2159
return CommonInventory._make_delta(self, old)
2157
2161
for key, old_value, self_value in \
2158
self.id_to_entry.iter_changes(old.id_to_entry):
2162
self.id_to_entry.iter_changes(old.id_to_entry):
2159
2163
file_id = key[0]
2160
2164
if old_value is not None:
2161
2165
old_path = old.id2path(file_id)
2174
2178
def path2id(self, relpath):
2175
2179
"""See CommonInventory.path2id()."""
2176
2180
# TODO: perhaps support negative hits?
2177
if isinstance(relpath, (str, text_type)):
2178
names = osutils.splitpath(relpath)
2183
relpath = osutils.pathjoin(*relpath)
2184
2181
result = self._path_to_fileid_cache.get(relpath, None)
2185
2182
if result is not None:
2184
if isinstance(relpath, basestring):
2185
names = osutils.splitpath(relpath)
2187
2188
current_id = self.root_id
2188
2189
if current_id is None:
2214
2215
def to_lines(self):
2215
2216
"""Serialise the inventory to lines."""
2216
lines = [b"chkinventory:\n"]
2217
if self._search_key_name != b'plain':
2217
lines = ["chkinventory:\n"]
2218
if self._search_key_name != 'plain':
2218
2219
# 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],))
2220
lines.append('search_key_name: %s\n' % (self._search_key_name,))
2221
lines.append("root_id: %s\n" % self.root_id)
2222
lines.append('parent_id_basename_to_file_id: %s\n' %
2223
(self.parent_id_basename_to_file_id.key()[0],))
2224
lines.append("revision_id: %s\n" % self.revision_id)
2225
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)
2227
lines.append("revision_id: %s\n" % self.revision_id)
2228
lines.append("root_id: %s\n" % self.root_id)
2229
2229
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],))
2230
lines.append('parent_id_basename_to_file_id: %s\n' %
2231
(self.parent_id_basename_to_file_id.key()[0],))
2232
lines.append("id_to_entry: %s\n" % (self.id_to_entry.key()[0],))
2236
2236
def root(self):
2237
2237
"""Get the root entry."""
2238
return self.get_entry(self.root_id)
2238
return self[self.root_id]
2241
2241
class CHKInventoryDirectory(InventoryDirectory):
2242
2242
"""A directory in an inventory."""
2244
__slots__ = ['_children', '_chk_inventory']
2244
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
2245
'text_id', 'parent_id', '_children', 'executable',
2246
'revision', 'symlink_target', 'reference_revision',
2246
2249
def __init__(self, file_id, name, parent_id, chk_inventory):
2247
2250
# Don't call InventoryDirectory.__init__ - it isn't right for this
2249
2252
InventoryEntry.__init__(self, file_id, name, parent_id)
2250
2253
self._children = None
2254
self.kind = 'directory'
2251
2255
self._chk_inventory = chk_inventory
2265
2269
# No longer supported
2266
2270
if self._chk_inventory.parent_id_basename_to_file_id is None:
2267
2271
raise AssertionError("Inventories without"
2268
" parent_id_basename_to_file_id are no longer supported")
2272
" parent_id_basename_to_file_id are no longer supported")
2270
2274
# XXX: Todo - use proxy objects for the children rather than loading
2271
2275
# all when the attribute is referenced.
2272
2276
parent_id_index = self._chk_inventory.parent_id_basename_to_file_id
2273
2277
child_keys = set()
2274
2278
for (parent_id, name_utf8), file_id in parent_id_index.iteritems(
2275
key_filter=[StaticTuple(self.file_id,)]):
2279
key_filter=[StaticTuple(self.file_id,)]):
2276
2280
child_keys.add(StaticTuple(file_id,))
2278
2282
for file_id_key in child_keys:
2434
2439
entry = item[3]
2435
2440
if new_path is None and entry is not None:
2436
2441
raise errors.InconsistentDelta(item[0], item[1],
2437
"Entry with no new_path")
2442
"Entry with no new_path")
2438
2443
if new_path is not None and entry is None:
2439
2444
raise errors.InconsistentDelta(new_path, item[1],
2440
"new_path with no entry")
2445
"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())