23
23
# But those depend on its position within a particular inventory, and
24
24
# it would be nice not to need to hold the backpointer here.
26
from __future__ import absolute_import
28
26
# This should really be an id randomly assigned when the tree is
29
27
# created, but it's not for now.
30
ROOT_ID = b"TREE_ROOT"
32
from ..lazy_import import lazy_import
30
from bzrlib.lazy_import import lazy_import
33
31
lazy_import(globals(), """
40
from breezy.bzr import (
50
from ..sixish import (
56
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
59
57
class InventoryEntry(object):
89
87
>>> i = Inventory()
92
>>> i.add(InventoryDirectory(b'123', 'src', ROOT_ID))
90
>>> i.add(InventoryDirectory('123', 'src', ROOT_ID))
93
91
InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None)
94
>>> i.add(InventoryFile(b'2323', 'hello.c', parent_id='123'))
92
>>> i.add(InventoryFile('2323', 'hello.c', parent_id='123'))
95
93
InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None, revision=None)
96
94
>>> shouldbe = {0: '', 1: 'src', 2: 'src/hello.c'}
97
95
>>> for ix, j in enumerate(i.iter_entries()):
106
104
InventoryDirectory('2325', 'wibble', parent_id='123', revision=None)
107
105
>>> i.path2id('src/wibble')
109
109
>>> i.add(InventoryFile('2326', 'wibble.c', '2325'))
110
110
InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None, revision=None)
111
>>> i.get_entry('2326')
112
112
InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None, revision=None)
113
113
>>> for path, entry in i.iter_entries():
131
131
RENAMED = 'renamed'
132
132
MODIFIED_AND_RENAMED = 'modified and renamed'
134
__slots__ = ['file_id', 'revision', 'parent_id', 'name']
136
# Attributes that all InventoryEntry instances are expected to have, but
137
# that don't vary for all kinds of entry. (e.g. symlink_target is only
138
# relevant to InventoryLink, so there's no reason to make every
139
# InventoryFile instance allocate space to hold a value for it.)
140
# Attributes that only vary for files: executable, text_sha1, text_size,
146
# Attributes that only vary for symlinks: symlink_target
147
symlink_target = None
148
# Attributes that only vary for tree-references: reference_revision
149
reference_revision = None
151
136
def detect_changes(self, old_entry):
152
137
"""Return a (text_modified, meta_modified) from this to old_entry.
194
176
candidates[ie.revision] = ie
195
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)
197
189
def has_text(self):
198
190
"""Return true if the object this entry represents has textual data.
208
def __init__(self, file_id, name, parent_id):
200
def __init__(self, file_id, name, parent_id, text_id=None):
209
201
"""Create an InventoryEntry
211
203
The filename must be a single component, relative to the
212
204
parent directory; it cannot be a whole path or relative name.
214
>>> e = InventoryFile(b'123', 'hello.c', ROOT_ID)
206
>>> e = InventoryFile('123', 'hello.c', ROOT_ID)
219
>>> e = InventoryFile(b'123', 'src/hello.c', ROOT_ID)
211
>>> e = InventoryFile('123', 'src/hello.c', ROOT_ID)
220
212
Traceback (most recent call last):
221
213
InvalidEntryName: Invalid entry name: src/hello.c
215
if '/' in name or '\\' in name:
224
216
raise errors.InvalidEntryName(name=name)
225
if not isinstance(file_id, bytes):
226
raise TypeError(file_id)
217
self.executable = False
219
self.text_sha1 = None
220
self.text_size = None
227
221
self.file_id = file_id
223
self.text_id = text_id
230
224
self.parent_id = parent_id
225
self.symlink_target = None
226
self.reference_revision = None
232
228
def kind_character(self):
233
229
"""Return a short kind indicator useful for appending to names."""
234
raise errors.BzrError('unknown kind %r' % self.kind)
230
raise BzrError('unknown kind %r' % self.kind)
236
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())
239
261
def versionable_kind(kind):
240
262
return (kind in ('file', 'directory', 'symlink', 'tree-reference'))
326
347
if not isinstance(other, InventoryEntry):
327
348
return NotImplemented
329
return ((self.file_id == other.file_id) and
330
(self.name == other.name) and
331
(other.symlink_target == self.symlink_target) and
332
(self.text_sha1 == other.text_sha1) and
333
(self.text_size == other.text_size) and
334
(self.text_id == other.text_id) and
335
(self.parent_id == other.parent_id) and
336
(self.kind == other.kind) and
337
(self.revision == other.revision) and
338
(self.executable == other.executable) and
339
(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)
342
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)
379
428
class InventoryDirectory(InventoryEntry):
380
429
"""A directory in an inventory."""
382
__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']
386
435
def _check(self, checker, rev_id):
387
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))
388
441
# In non rich root repositories we do not expect a file graph for the
390
443
if self.name == '' and not checker.rich_roots:
393
446
# to provide a per-fileid log. The hash of every directory content is
394
447
# "da..." below (the sha1sum of '').
395
448
checker.add_pending_item(rev_id,
396
('texts', self.file_id, self.revision), b'text',
397
b'da39a3ee5e6b4b0d3255bfef95601890afd80709')
449
('texts', self.file_id, self.revision), 'text',
450
'da39a3ee5e6b4b0d3255bfef95601890afd80709')
400
453
other = InventoryDirectory(self.file_id, self.name, self.parent_id)
406
459
def __init__(self, file_id, name, parent_id):
407
460
super(InventoryDirectory, self).__init__(file_id, name, parent_id)
408
461
self.children = {}
410
def sorted_children(self):
411
return sorted(viewitems(self.children))
462
self.kind = 'directory'
413
464
def kind_character(self):
414
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."""
418
482
class InventoryFile(InventoryEntry):
419
483
"""A file in an inventory."""
421
__slots__ = ['text_sha1', 'text_size', 'text_id', 'executable']
425
def __init__(self, file_id, name, parent_id):
426
super(InventoryFile, self).__init__(file_id, name, parent_id)
427
self.text_sha1 = None
428
self.text_size = None
430
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']
432
489
def _check(self, checker, tree_revision_id):
433
490
"""See InventoryEntry._check"""
434
491
# TODO: check size too.
435
492
checker.add_pending_item(tree_revision_id,
436
('texts', self.file_id, self.revision), b'text',
493
('texts', self.file_id, self.revision), 'text',
438
495
if self.text_size is None:
439
496
checker._report_items.append(
440
497
'fileid {%s} in {%s} has None for text_size' % (self.file_id,
444
501
other = InventoryFile(self.file_id, self.name, self.parent_id)
456
513
return text_modified, meta_modified
458
515
def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
459
output_to, reverse=False):
516
output_to, reverse=False):
460
517
"""See InventoryEntry._diff."""
461
from breezy.diff import DiffText
518
from bzrlib.diff import DiffText
462
519
from_file_id = self.file_id
464
521
to_file_id = to_entry.file_id
465
to_path = to_tree.id2path(to_file_id)
467
523
to_file_id = None
469
if from_file_id is not None:
470
from_path = tree.id2path(from_file_id)
474
525
to_file_id, from_file_id = from_file_id, to_file_id
475
526
tree, to_tree = to_tree, tree
476
527
from_label, to_label = to_label, from_label
477
528
differ = DiffText(tree, to_tree, output_to, 'utf-8', '', '',
479
return differ.diff_text(from_path, to_path, from_label, to_label,
480
from_file_id, to_file_id)
530
return differ.diff_text(from_file_id, to_file_id, from_label, to_label)
482
532
def has_text(self):
483
533
"""See InventoryEntry.has_text."""
536
def __init__(self, file_id, name, parent_id):
537
super(InventoryFile, self).__init__(file_id, name, parent_id)
486
540
def kind_character(self):
487
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)
490
561
def _read_tree_state(self, path, work_tree):
491
562
"""See InventoryEntry._read_tree_state."""
492
self.text_sha1 = work_tree.get_file_sha1(path, self.file_id)
563
self.text_sha1 = work_tree.get_file_sha1(self.file_id, path=path)
493
564
# FIXME: 20050930 probe for the text size when getting sha1
494
565
# in _read_tree_state
495
self.executable = work_tree.is_executable(path, self.file_id)
566
self.executable = work_tree.is_executable(self.file_id, path=path)
497
568
def __repr__(self):
498
569
return ("%s(%r, %r, parent_id=%r, sha1=%r, len=%s, revision=%s)"
524
595
class InventoryLink(InventoryEntry):
525
596
"""A file in an inventory."""
527
__slots__ = ['symlink_target']
531
def __init__(self, file_id, name, parent_id):
532
super(InventoryLink, self).__init__(file_id, name, parent_id)
533
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']
535
602
def _check(self, checker, tree_revision_id):
536
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))
537
608
if self.symlink_target is None:
538
609
checker._report_items.append(
539
610
'symlink {%s} has no target in revision {%s}'
540
% (self.file_id, tree_revision_id))
611
% (self.file_id, tree_revision_id))
541
612
# Symlinks are stored as ''
542
613
checker.add_pending_item(tree_revision_id,
543
('texts', self.file_id, self.revision), b'text',
544
b'da39a3ee5e6b4b0d3255bfef95601890afd80709')
614
('texts', self.file_id, self.revision), 'text',
615
'da39a3ee5e6b4b0d3255bfef95601890afd80709')
547
618
other = InventoryLink(self.file_id, self.name, self.parent_id)
554
625
# FIXME: which _modified field should we use ? RBC 20051003
555
626
text_modified = (self.symlink_target != old_entry.symlink_target)
556
627
if text_modified:
557
trace.mutter(" symlink target changed")
628
mutter(" symlink target changed")
558
629
meta_modified = False
559
630
return text_modified, meta_modified
561
632
def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
562
output_to, reverse=False):
633
output_to, reverse=False):
563
634
"""See InventoryEntry._diff."""
564
from breezy.diff import DiffSymlink
635
from bzrlib.diff import DiffSymlink
565
636
old_target = self.symlink_target
566
637
if to_entry is not None:
567
638
new_target = to_entry.symlink_target
577
648
differ = DiffSymlink(old_tree, new_tree, output_to)
578
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'
580
655
def kind_character(self):
581
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))
584
675
def _read_tree_state(self, path, work_tree):
585
676
"""See InventoryEntry._read_tree_state."""
586
self.symlink_target = work_tree.get_symlink_target(
587
work_tree.id2path(self.file_id), self.file_id)
677
self.symlink_target = work_tree.get_symlink_target(self.file_id)
589
679
def _forget_tree_state(self):
590
680
self.symlink_target = None
649
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)
652
753
def has_filename(self, filename):
653
754
return bool(self.path2id(filename))
656
757
"""Return as a string the path to file_id.
658
759
>>> i = Inventory()
659
>>> e = i.add(InventoryDirectory(b'src-id', 'src', ROOT_ID))
660
>>> e = i.add(InventoryFile(b'foo-id', 'foo.c', parent_id='src-id'))
661
>>> 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')
664
765
:raises NoSuchId: If file_id is not present in the inventory.
719
822
# if we finished all children, pop it off the stack
722
def _preload_cache(self):
723
"""Populate any caches, we are about to access all items.
725
The default implementation does nothing, because CommonInventory doesn't
730
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):
731
827
"""Iterate over the entries in a directory first order.
733
829
This returns all entries for a directory before returning
735
831
lexicographically sorted order, and is a hybrid between
736
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.
738
837
:return: This yields (path, entry) pairs
740
839
if specific_file_ids and not isinstance(specific_file_ids, set):
741
840
specific_file_ids = set(specific_file_ids)
742
841
# TODO? Perhaps this should return the from_dir so that the root is
743
842
# yielded? or maybe an option?
744
if from_dir is None and specific_file_ids is None:
745
# They are iterating from the root, and have not specified any
746
# specific entries to look at. All current callers fully consume the
747
# iterator, so we can safely assume we are accessing all entries
748
self._preload_cache()
749
843
if from_dir is None:
750
844
if self.root is None:
752
846
# Optimize a common case
753
if (specific_file_ids is not None
754
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):
755
849
file_id = list(specific_file_ids)[0]
756
if file_id is not None:
758
path = self.id2path(file_id)
759
except errors.NoSuchId:
762
yield path, self.get_entry(file_id)
851
yield self.id2path(file_id), self[file_id]
764
853
from_dir = self.root
765
if (specific_file_ids is None
766
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):
767
856
yield u'', self.root
768
elif isinstance(from_dir, bytes):
769
from_dir = self.get_entry(from_dir)
771
raise TypeError(from_dir)
857
elif isinstance(from_dir, basestring):
858
from_dir = self[from_dir]
773
860
if specific_file_ids is not None:
774
861
# TODO: jam 20070302 This could really be done as a loop rather
775
862
# than a bunch of recursive calls.
779
865
def add_ancestors(file_id):
780
if not byid.has_id(file_id):
866
if file_id not in byid:
782
parent_id = byid.get_entry(file_id).parent_id
868
parent_id = byid[file_id].parent_id
783
869
if parent_id is None:
785
871
if parent_id not in parents:
795
881
cur_relpath, cur_dir = stack.pop()
798
for child_name, child_ie in sorted(viewitems(cur_dir.children)):
884
for child_name, child_ie in sorted(cur_dir.children.iteritems()):
800
886
child_relpath = cur_relpath + child_name
802
if (specific_file_ids is None
803
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)):
804
891
yield child_relpath, child_ie
806
893
if child_ie.kind == 'directory':
807
894
if parents is None or child_ie.file_id in parents:
808
child_dirs.append((child_relpath + '/', child_ie))
895
child_dirs.append((child_relpath+'/', child_ie))
809
896
stack.extend(reversed(child_dirs))
811
898
def _make_delta(self, old):
812
899
"""Make an inventory delta from two inventories."""
813
old_ids = set(old.iter_all_ids())
814
new_ids = set(self.iter_all_ids())
815
902
adds = new_ids - old_ids
816
903
deletes = old_ids - new_ids
817
904
common = old_ids.intersection(new_ids)
819
906
for file_id in deletes:
820
907
delta.append((old.id2path(file_id), None, file_id, None))
821
908
for file_id in adds:
822
delta.append((None, self.id2path(file_id),
823
file_id, self.get_entry(file_id)))
909
delta.append((None, self.id2path(file_id), file_id, self[file_id]))
824
910
for file_id in common:
825
if old.get_entry(file_id) != self.get_entry(file_id):
911
if old[file_id] != self[file_id]:
826
912
delta.append((old.id2path(file_id), self.id2path(file_id),
827
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)
830
924
def make_entry(self, kind, name, parent_id, file_id=None):
831
"""Simple thunk to breezy.bzr.inventory.make_entry."""
925
"""Simple thunk to bzrlib.inventory.make_entry."""
832
926
return make_entry(kind, name, parent_id, file_id)
834
928
def entries(self):
837
931
This may be faster than iter_entries.
841
934
def descend(dir_ie, dir_path):
842
kids = sorted(viewitems(dir_ie.children))
935
kids = dir_ie.children.items()
843
937
for name, ie in kids:
844
938
child_path = osutils.pathjoin(dir_path, name)
845
939
accum.append((child_path, ie))
846
940
if ie.kind == 'directory':
847
941
descend(ie, child_path)
849
if self.root is not None:
850
descend(self.root, u'')
853
def get_entry_by_path(self, relpath):
854
"""Return an inventory entry by path.
943
descend(self.root, u'')
946
def directories(self):
947
"""Return (path, entry) pairs for all directories, including the root.
950
def descend(parent_ie, parent_path):
951
accum.append((parent_path, parent_ie))
953
kids = [(ie.name, ie) for ie in parent_ie.children.itervalues() if ie.kind == 'directory']
956
for name, child_ie in kids:
957
child_path = osutils.pathjoin(parent_path, name)
958
descend(child_ie, child_path)
959
descend(self.root, u'')
962
def path2id(self, relpath):
963
"""Walk down through directories to return entry of last component.
856
965
:param relpath: may be either a list of path components, or a single
857
966
string, in which case it is automatically split.
884
993
# or raise an error?
888
def path2id(self, relpath):
889
"""Walk down through directories to return entry of last component.
891
:param relpath: may be either a list of path components, or a single
892
string, in which case it is automatically split.
894
This returns the entry of the last component in the path,
895
which may be either a file or a directory.
897
Returns None IFF the path is not found.
899
ie = self.get_entry_by_path(relpath)
996
return parent.file_id
904
998
def filter(self, specific_fileids):
905
999
"""Get an inventory view filtered against a set of file-ids.
919
1013
entries = self.iter_entries()
920
1014
if self.root is None:
921
1015
return Inventory(root_id=None)
922
other = Inventory(next(entries)[1].file_id)
1016
other = Inventory(entries.next()[1].file_id)
923
1017
other.root.revision = self.root.revision
924
1018
other.revision_id = self.revision_id
925
1019
directories_to_expand = set()
926
1020
for path, entry in entries:
927
1021
file_id = entry.file_id
928
if (file_id in specific_fileids or
929
entry.parent_id in directories_to_expand):
1022
if (file_id in specific_fileids
1023
or entry.parent_id in directories_to_expand):
930
1024
if entry.kind == 'directory':
931
1025
directories_to_expand.add(file_id)
932
1026
elif file_id not in interesting_parents:
957
1051
returned quickly.
959
1053
>>> inv = Inventory()
960
>>> inv.add(InventoryFile(b'123-123', 'hello.c', ROOT_ID))
1054
>>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
961
1055
InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
962
>>> inv.get_entry(b'123-123').name
1056
>>> inv['123-123'].name
965
1059
Id's may be looked up from paths:
967
1061
>>> inv.path2id('hello.c')
969
>>> inv.has_id(b'123-123')
1063
>>> '123-123' in inv
972
1066
There are iterators over the contents:
1014
1108
applied the final inventory must be internally consistent, but it
1015
1109
is ok to supply changes which, if only half-applied would have an
1016
1110
invalid result - such as supplying two changes which rename two
1017
files, 'A' and 'B' with each other : [('A', 'B', b'A-id', a_entry),
1018
('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)].
1020
1114
Each change is a tuple, of the form (old_path, new_path, file_id,
1060
1154
# after their children, which means that everything we examine has no
1061
1155
# modified children remaining by the time we examine it.
1062
1156
for old_path, file_id in sorted(((op, f) for op, np, f, e in delta
1063
if op is not None), reverse=True):
1157
if op is not None), reverse=True):
1064
1158
# Preserve unaltered children of file_id for later reinsertion.
1065
file_id_children = getattr(self.get_entry(file_id), 'children', {})
1159
file_id_children = getattr(self[file_id], 'children', {})
1066
1160
if len(file_id_children):
1067
1161
children[file_id] = file_id_children
1068
1162
if self.id2path(file_id) != old_path:
1069
1163
raise errors.InconsistentDelta(old_path, file_id,
1070
"Entry was at wrong other path %r." % self.id2path(file_id))
1164
"Entry was at wrong other path %r." % self.id2path(file_id))
1071
1165
# Remove file_id and the unaltered children. If file_id is not
1072
1166
# being deleted it will be reinserted back later.
1073
1167
self.remove_recursive_id(file_id)
1077
1171
# the resulting inventory were also modified, are inserted after their
1079
1173
for new_path, f, new_entry in sorted((np, f, e) for op, np, f, e in
1080
delta if np is not None):
1174
delta if np is not None):
1081
1175
if new_entry.kind == 'directory':
1082
1176
# Pop the child which to allow detection of children whose
1083
1177
# parents were deleted and which were not reattached to a new
1085
1179
replacement = InventoryDirectory(new_entry.file_id,
1086
new_entry.name, new_entry.parent_id)
1180
new_entry.name, new_entry.parent_id)
1087
1181
replacement.revision = new_entry.revision
1088
1182
replacement.children = children.pop(replacement.file_id, {})
1089
1183
new_entry = replacement
1091
1185
self.add(new_entry)
1092
1186
except errors.DuplicateFileId:
1093
1187
raise errors.InconsistentDelta(new_path, new_entry.file_id,
1094
"New id is already present in target.")
1188
"New id is already present in target.")
1095
1189
except AttributeError:
1096
1190
raise errors.InconsistentDelta(new_path, new_entry.file_id,
1097
"Parent is not a directory.")
1191
"Parent is not a directory.")
1098
1192
if self.id2path(new_entry.file_id) != new_path:
1099
1193
raise errors.InconsistentDelta(new_path, new_entry.file_id,
1100
"New path is not consistent with parent path.")
1194
"New path is not consistent with parent path.")
1101
1195
if len(children):
1102
1196
# Get the parent id that was deleted
1103
1197
parent_id, children = children.popitem()
1104
1198
raise errors.InconsistentDelta("<deleted>", parent_id,
1105
"The file id was deleted but its children were not deleted.")
1199
"The file id was deleted but its children were not deleted.")
1107
1201
def create_by_apply_delta(self, inventory_delta, new_revision_id,
1108
1202
propagate_caches=False):
1129
1223
other.add(entry.copy())
1132
def iter_all_ids(self):
1226
def _get_mutable_inventory(self):
1227
"""See CommonInventory._get_mutable_inventory."""
1228
return copy.deepcopy(self)
1133
1231
"""Iterate over all file-ids."""
1134
1232
return iter(self._byid)
1136
1234
def iter_just_entries(self):
1137
1235
"""Iterate over all entries.
1139
1237
Unlike iter_entries(), just the entries are returned (not (path, ie))
1140
1238
and the order of entries is undefined.
1142
1240
XXX: We may not want to merge this into bzr.dev.
1144
1242
if self.root is None:
1146
return iter(viewvalues(self._byid))
1244
for _, ie in self._byid.iteritems():
1148
1247
def __len__(self):
1149
1248
"""Returns number of entries."""
1150
1249
return len(self._byid)
1152
def get_entry(self, file_id):
1251
def __getitem__(self, file_id):
1153
1252
"""Return the entry for given file_id.
1155
1254
>>> inv = Inventory()
1156
>>> inv.add(InventoryFile(b'123123', 'hello.c', ROOT_ID))
1255
>>> inv.add(InventoryFile('123123', 'hello.c', ROOT_ID))
1157
1256
InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
1158
>>> inv.get_entry(b'123123').name
1257
>>> inv['123123'].name
1161
if not isinstance(file_id, bytes):
1162
raise TypeError(file_id)
1164
1261
return self._byid[file_id]
1165
1262
except KeyError:
1170
1267
return self._byid[file_id].kind
1172
1269
def get_child(self, parent_id, filename):
1173
return self.get_entry(parent_id).children.get(filename)
1270
return self[parent_id].children.get(filename)
1175
1272
def _add_child(self, entry):
1176
1273
"""Add an entry to the inventory, without adding it to its parent"""
1177
1274
if entry.file_id in self._byid:
1178
raise errors.BzrError(
1179
"inventory already contains entry with id {%s}" %
1275
raise BzrError("inventory already contains entry with id {%s}" %
1181
1277
self._byid[entry.file_id] = entry
1182
children = getattr(entry, 'children', {})
1183
if children is not None:
1184
for child in viewvalues(children):
1185
self._add_child(child)
1278
for child in getattr(entry, 'children', {}).itervalues():
1279
self._add_child(child)
1188
1282
def add(self, entry):
1189
1283
"""Add entry to inventory.
1285
To add a file to a branch ready to be committed, use Branch.add,
1193
1290
if entry.file_id in self._byid:
1232
1329
ie = make_entry(kind, parts[-1], parent_id, file_id)
1233
1330
return self.add(ie)
1235
def delete(self, file_id):
1332
def __delitem__(self, file_id):
1236
1333
"""Remove entry by id.
1238
1335
>>> inv = Inventory()
1239
>>> inv.add(InventoryFile(b'123', 'foo.c', ROOT_ID))
1336
>>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
1240
1337
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
1241
>>> inv.has_id(b'123')
1243
>>> inv.delete(b'123')
1244
>>> inv.has_id(b'123')
1247
ie = self.get_entry(file_id)
1248
1345
del self._byid[file_id]
1249
1346
if ie.parent_id is not None:
1250
del self.get_entry(ie.parent_id).children[ie.name]
1347
del self[ie.parent_id].children[ie.name]
1252
1349
def __eq__(self, other):
1253
1350
"""Compare two sets by comparing their contents.
1256
1353
>>> i2 = Inventory()
1259
>>> i1.add(InventoryFile(b'123', 'foo', ROOT_ID))
1356
>>> i1.add(InventoryFile('123', 'foo', ROOT_ID))
1260
1357
InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
1263
>>> i2.add(InventoryFile(b'123', 'foo', ROOT_ID))
1360
>>> i2.add(InventoryFile('123', 'foo', ROOT_ID))
1264
1361
InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
1292
1389
def _make_delta(self, old):
1293
1390
"""Make an inventory delta from two inventories."""
1294
old_getter = old.get_entry
1295
new_getter = self.get_entry
1296
old_ids = set(old.iter_all_ids())
1297
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)
1298
1395
adds = new_ids - old_ids
1299
1396
deletes = old_ids - new_ids
1300
1397
if not adds and not deletes:
1305
1402
for file_id in deletes:
1306
1403
delta.append((old.id2path(file_id), None, file_id, None))
1307
1404
for file_id in adds:
1308
delta.append((None, self.id2path(file_id),
1309
file_id, self.get_entry(file_id)))
1405
delta.append((None, self.id2path(file_id), file_id, self[file_id]))
1310
1406
for file_id in common:
1311
new_ie = new_getter(file_id)
1312
old_ie = old_getter(file_id)
1407
new_ie = new_getter[file_id]
1408
old_ie = old_getter[file_id]
1313
1409
# If xml_serializer returns the cached InventoryEntries (rather
1314
1410
# than always doing .copy()), inlining the 'is' check saves 2.7M
1315
1411
# calls to __eq__. Under lsprof this saves 20s => 6s.
1351
1447
new_name = ensure_normalized_name(new_name)
1352
1448
if not is_valid_name(new_name):
1353
raise errors.BzrError("not an acceptable filename: %r" % new_name)
1449
raise BzrError("not an acceptable filename: %r" % new_name)
1355
1451
new_parent = self._byid[new_parent_id]
1356
1452
if new_name in new_parent.children:
1357
raise errors.BzrError("%r already exists in %r" %
1358
(new_name, self.id2path(new_parent_id)))
1453
raise BzrError("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
1360
1455
new_parent_idpath = self.get_idpath(new_parent_id)
1361
1456
if file_id in new_parent_idpath:
1362
raise errors.BzrError(
1363
"cannot move directory %r into a subdirectory of itself, %r"
1364
% (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)))
1366
1460
file_ie = self._byid[file_id]
1367
1461
old_parent = self._byid[file_ie.parent_id]
1438
1531
if entry.parent_id is not None:
1439
1532
parent_str = entry.parent_id
1442
1535
name_str = entry.name.encode("utf8")
1443
1536
if entry.kind == 'file':
1444
1537
if entry.executable:
1448
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" % (
1449
1542
entry.file_id, parent_str, name_str, entry.revision,
1450
1543
entry.text_sha1, entry.text_size, exec_str)
1451
1544
elif entry.kind == 'directory':
1452
return b"dir: %s\n%s\n%s\n%s" % (
1545
return "dir: %s\n%s\n%s\n%s" % (
1453
1546
entry.file_id, parent_str, name_str, entry.revision)
1454
1547
elif entry.kind == 'symlink':
1455
return b"symlink: %s\n%s\n%s\n%s\n%s" % (
1548
return "symlink: %s\n%s\n%s\n%s\n%s" % (
1456
1549
entry.file_id, parent_str, name_str, entry.revision,
1457
1550
entry.symlink_target.encode("utf8"))
1458
1551
elif entry.kind == 'tree-reference':
1459
return b"tree: %s\n%s\n%s\n%s\n%s" % (
1552
return "tree: %s\n%s\n%s\n%s\n%s" % (
1460
1553
entry.file_id, parent_str, name_str, entry.revision,
1461
1554
entry.reference_revision)
1504
1597
remaining_parents = interesting.difference(file_ids)
1505
1598
# When we hit the TREE_ROOT, we'll get an interesting parent of None,
1506
1599
# but we don't actually want to recurse into that
1507
interesting.add(None) # this will auto-filter it in the loop
1508
remaining_parents.discard(None)
1600
interesting.add(None) # this will auto-filter it in the loop
1601
remaining_parents.discard(None)
1509
1602
while remaining_parents:
1510
1603
next_parents = set()
1511
1604
for entry in self._getitems(remaining_parents):
1512
1605
next_parents.add(entry.parent_id)
1513
children_of_parent_id.setdefault(entry.parent_id, set()
1514
).add(entry.file_id)
1606
children_of_parent_id.setdefault(entry.parent_id, []
1607
).append(entry.file_id)
1515
1608
# Remove any search tips we've already processed
1516
1609
remaining_parents = next_parents.difference(interesting)
1517
1610
interesting.update(remaining_parents)
1524
1617
keys = [StaticTuple(f,).intern() for f in directories_to_expand]
1525
1618
directories_to_expand = set()
1526
1619
items = self.parent_id_basename_to_file_id.iteritems(keys)
1527
next_file_ids = {item[1] for item in items}
1620
next_file_ids = set([item[1] for item in items])
1528
1621
next_file_ids = next_file_ids.difference(interesting)
1529
1622
interesting.update(next_file_ids)
1530
1623
for entry in self._getitems(next_file_ids):
1531
1624
if entry.kind == 'directory':
1532
1625
directories_to_expand.add(entry.file_id)
1533
children_of_parent_id.setdefault(entry.parent_id, set()
1534
).add(entry.file_id)
1626
children_of_parent_id.setdefault(entry.parent_id, []
1627
).append(entry.file_id)
1535
1628
return interesting, children_of_parent_id
1537
1630
def filter(self, specific_fileids):
1559
1652
# parent_to_children with at least the tree root.)
1561
1654
cache = self._fileid_to_entry_cache
1562
remaining_children = collections.deque(
1563
parent_to_children[self.root_id])
1656
remaining_children = collections.deque(parent_to_children[self.root_id])
1658
import pdb; pdb.set_trace()
1564
1660
while remaining_children:
1565
1661
file_id = remaining_children.popleft()
1566
1662
ie = cache[file_id]
1567
1663
if ie.kind == 'directory':
1568
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
1569
1665
# TODO: depending on the uses of 'other' we should probably alwyas
1570
1666
# '.copy()' to prevent someone from mutating other and
1571
1667
# invaliding our internal cache
1578
def _bytes_to_utf8name_key(data):
1579
"""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."""
1580
1676
# We don't normally care about name, except for times when we want
1581
1677
# to filter out empty names because of non rich-root...
1582
sections = data.split(b'\n')
1583
kind, file_id = sections[0].split(b': ')
1584
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]))
1586
1682
def _bytes_to_entry(self, bytes):
1587
1683
"""Deserialise a serialised entry."""
1588
sections = bytes.split(b'\n')
1589
if sections[0].startswith(b"file: "):
1684
sections = bytes.split('\n')
1685
if sections[0].startswith("file: "):
1590
1686
result = InventoryFile(sections[0][6:],
1591
sections[2].decode('utf8'),
1687
sections[2].decode('utf8'),
1593
1689
result.text_sha1 = sections[4]
1594
1690
result.text_size = int(sections[5])
1595
result.executable = sections[6] == b"Y"
1596
elif sections[0].startswith(b"dir: "):
1691
result.executable = sections[6] == "Y"
1692
elif sections[0].startswith("dir: "):
1597
1693
result = CHKInventoryDirectory(sections[0][5:],
1598
sections[2].decode('utf8'),
1600
elif sections[0].startswith(b"symlink: "):
1694
sections[2].decode('utf8'),
1696
elif sections[0].startswith("symlink: "):
1601
1697
result = InventoryLink(sections[0][9:],
1602
sections[2].decode('utf8'),
1698
sections[2].decode('utf8'),
1604
1700
result.symlink_target = sections[4].decode('utf8')
1605
elif sections[0].startswith(b"tree: "):
1701
elif sections[0].startswith("tree: "):
1606
1702
result = TreeReference(sections[0][6:],
1607
sections[2].decode('utf8'),
1703
sections[2].decode('utf8'),
1609
1705
result.reference_revision = sections[4]
1611
1707
raise ValueError("Not a serialised entry %r" % bytes)
1612
result.file_id = bytesintern(result.file_id)
1613
result.revision = bytesintern(sections[3])
1614
if result.parent_id == b'':
1708
result.file_id = intern(result.file_id)
1709
result.revision = intern(sections[3])
1710
if result.parent_id == '':
1615
1711
result.parent_id = None
1616
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())
1619
1723
def create_by_apply_delta(self, inventory_delta, new_revision_id,
1620
propagate_caches=False):
1724
propagate_caches=False):
1621
1725
"""Create a new CHKInventory by applying inventory_delta to this one.
1623
1727
See the inventory developers documentation for the theory behind
1749
1852
new_key, [None, None])[1] = new_value
1750
1853
# validate that deletes are complete.
1751
1854
for file_id in deletes:
1752
entry = self.get_entry(file_id)
1855
entry = self[file_id]
1753
1856
if entry.kind != 'directory':
1755
1858
# This loop could potentially be better by using the id_basename
1756
1859
# map to just get the child file ids.
1757
for child in viewvalues(entry.children):
1860
for child in entry.children.values():
1758
1861
if child.file_id not in altered:
1759
1862
raise errors.InconsistentDelta(self.id2path(child.file_id),
1760
child.file_id, "Child not deleted or reparented when "
1863
child.file_id, "Child not deleted or reparented when "
1762
1865
result.id_to_entry.apply_delta(id_to_entry_delta)
1763
1866
if parent_id_basename_delta:
1764
1867
# Transform the parent_id_basename delta data into a linear delta
1775
1878
parents.discard(('', None))
1776
1879
for parent_path, parent in parents:
1778
if result.get_entry(parent).kind != 'directory':
1881
if result[parent].kind != 'directory':
1779
1882
raise errors.InconsistentDelta(result.id2path(parent), parent,
1780
'Not a directory, but given children')
1883
'Not a directory, but given children')
1781
1884
except errors.NoSuchId:
1782
1885
raise errors.InconsistentDelta("<unknown>", parent,
1783
"Parent is not present in resulting inventory.")
1886
"Parent is not present in resulting inventory.")
1784
1887
if result.path2id(parent_path) != parent:
1785
1888
raise errors.InconsistentDelta(parent_path, parent,
1786
"Parent has wrong path %r." % result.path2id(parent_path))
1889
"Parent has wrong path %r." % result.path2id(parent_path))
1797
1900
:return: A CHKInventory
1799
lines = bytes.split(b'\n')
1800
if lines[-1] != b'':
1902
lines = bytes.split('\n')
1801
1904
raise AssertionError('bytes to deserialize must end with an eol')
1803
if lines[0] != b'chkinventory:':
1906
if lines[0] != 'chkinventory:':
1804
1907
raise ValueError("not a serialised CHKInventory: %r" % bytes)
1806
allowed_keys = frozenset((b'root_id', b'revision_id',
1807
b'parent_id_basename_to_file_id',
1808
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',
1809
1912
for line in lines[1:]:
1810
key, value = line.split(b': ', 1)
1913
key, value = line.split(': ', 1)
1811
1914
if key not in allowed_keys:
1812
1915
raise errors.BzrError('Unknown key in inventory: %r\n%r'
1813
1916
% (key, bytes))
1815
1918
raise errors.BzrError('Duplicate key in inventory: %r\n%r'
1816
1919
% (key, bytes))
1817
1920
info[key] = value
1818
revision_id = bytesintern(info[b'revision_id'])
1819
root_id = bytesintern(info[b'root_id'])
1820
search_key_name = bytesintern(info.get(b'search_key_name', b'plain'))
1821
parent_id_basename_to_file_id = bytesintern(info.get(
1822
b'parent_id_basename_to_file_id', None))
1823
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:'):
1824
1927
raise ValueError('parent_id_basename_to_file_id should be a sha1'
1825
1928
' key not %r' % (parent_id_basename_to_file_id,))
1826
id_to_entry = info[b'id_to_entry']
1827
if not id_to_entry.startswith(b'sha1:'):
1929
id_to_entry = info['id_to_entry']
1930
if not id_to_entry.startswith('sha1:'):
1828
1931
raise ValueError('id_to_entry should be a sha1'
1829
1932
' key not %r' % (id_to_entry,))
1845
1948
search_key_func=search_key_func)
1846
1949
if (result.revision_id,) != expected_revision_id:
1847
1950
raise ValueError("Mismatched revision id and expected: %r, %r" %
1848
(result.revision_id, expected_revision_id))
1951
(result.revision_id, expected_revision_id))
1852
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'):
1853
1956
"""Create a CHKInventory from an existing inventory.
1855
1958
The content of inventory is copied into the chk_store, and a
1875
1978
parent_id_basename_dict[p_id_key] = entry.file_id
1877
1980
result._populate_from_dicts(chk_store, id_to_entry_dict,
1878
parent_id_basename_dict, maximum_size=maximum_size)
1981
parent_id_basename_dict, maximum_size=maximum_size)
1881
1984
def _populate_from_dicts(self, chk_store, id_to_entry_dict,
1882
1985
parent_id_basename_dict, maximum_size):
1883
search_key_func = chk_map.search_key_registry.get(
1884
self._search_key_name)
1986
search_key_func = chk_map.search_key_registry.get(self._search_key_name)
1885
1987
root_key = chk_map.CHKMap.from_dict(chk_store, id_to_entry_dict,
1886
maximum_size=maximum_size, key_width=1,
1887
search_key_func=search_key_func)
1988
maximum_size=maximum_size, key_width=1,
1989
search_key_func=search_key_func)
1888
1990
self.id_to_entry = chk_map.CHKMap(chk_store, root_key,
1889
1991
search_key_func)
1890
1992
root_key = chk_map.CHKMap.from_dict(chk_store,
1891
parent_id_basename_dict,
1892
maximum_size=maximum_size, key_width=2,
1893
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)
1894
1996
self.parent_id_basename_to_file_id = chk_map.CHKMap(chk_store,
1895
root_key, search_key_func)
1997
root_key, search_key_func)
1897
1999
def _parent_id_basename_key(self, entry):
1898
2000
"""Create a key for a entry in a parent_id_basename_to_file_id index."""
1899
2001
if entry.parent_id is not None:
1900
2002
parent_id = entry.parent_id
1903
2005
return StaticTuple(parent_id, entry.name.encode('utf8')).intern()
1905
def get_entry(self, file_id):
2007
def __getitem__(self, file_id):
1906
2008
"""map a single file_id -> InventoryEntry."""
1907
2009
if file_id is None:
1908
2010
raise errors.NoSuchId(self, file_id)
1913
2015
return self._bytes_to_entry(
1914
next(self.id_to_entry.iteritems([StaticTuple(file_id,)]))[1])
2016
self.id_to_entry.iteritems([StaticTuple(file_id,)]).next()[1])
1915
2017
except StopIteration:
1916
2018
# really we're passing an inventory, not a tree...
1917
2019
raise errors.NoSuchId(self, file_id)
1919
2021
def _getitems(self, file_ids):
1920
"""Similar to get_entry, but lets you query for multiple.
2022
"""Similar to __getitem__, but lets you query for multiple.
1922
2024
The returned order is undefined. And currently if an item doesn't
1923
2025
exist, it isn't included in the output.
1951
2053
"""Yield the parents of file_id up to the root."""
1952
2054
while file_id is not None:
1954
ie = self.get_entry(file_id)
1955
2057
except KeyError:
1956
2058
raise errors.NoSuchId(tree=self, file_id=file_id)
1958
2060
file_id = ie.parent_id
1960
def iter_all_ids(self):
1961
2063
"""Iterate over all file-ids."""
1962
2064
for key, _ in self.id_to_entry.iteritems():
1965
2067
def iter_just_entries(self):
1966
2068
"""Iterate over all entries.
1968
2070
Unlike iter_entries(), just the entries are returned (not (path, ie))
1969
2071
and the order of entries is undefined.
1978
2080
self._fileid_to_entry_cache[file_id] = ie
1981
def _preload_cache(self):
1982
"""Make sure all file-ids are in _fileid_to_entry_cache"""
1983
if self._fully_cached:
1984
return # No need to do it again
1985
# The optimal sort order is to use iteritems() directly
1986
cache = self._fileid_to_entry_cache
1987
for key, entry in self.id_to_entry.iteritems():
1989
if file_id not in cache:
1990
ie = self._bytes_to_entry(entry)
1994
last_parent_id = last_parent_ie = None
1995
pid_items = self.parent_id_basename_to_file_id.iteritems()
1996
for key, child_file_id in pid_items:
1997
if key == (b'', b''): # This is the root
1998
if child_file_id != self.root_id:
1999
raise ValueError('Data inconsistency detected.'
2000
' We expected data with key ("","") to match'
2001
' the root id, but %s != %s'
2002
% (child_file_id, self.root_id))
2004
parent_id, basename = key
2005
ie = cache[child_file_id]
2006
if parent_id == last_parent_id:
2007
parent_ie = last_parent_ie
2009
parent_ie = cache[parent_id]
2010
if parent_ie.kind != 'directory':
2011
raise ValueError('Data inconsistency detected.'
2012
' An entry in the parent_id_basename_to_file_id map'
2013
' has parent_id {%s} but the kind of that object'
2014
' is %r not "directory"' % (parent_id, parent_ie.kind))
2015
if parent_ie._children is None:
2016
parent_ie._children = {}
2017
basename = basename.decode('utf-8')
2018
if basename in parent_ie._children:
2019
existing_ie = parent_ie._children[basename]
2020
if existing_ie != ie:
2021
raise ValueError('Data inconsistency detected.'
2022
' Two entries with basename %r were found'
2023
' in the parent entry {%s}'
2024
% (basename, parent_id))
2025
if basename != ie.name:
2026
raise ValueError('Data inconsistency detected.'
2027
' In the parent_id_basename_to_file_id map, file_id'
2028
' {%s} is listed as having basename %r, but in the'
2029
' id_to_entry map it is %r'
2030
% (child_file_id, basename, ie.name))
2031
parent_ie._children[basename] = ie
2032
self._fully_cached = True
2034
2083
def iter_changes(self, basis):
2035
2084
"""Generate a Tree.iter_changes change list between this and basis.
2081
2130
if kind[0] != kind[1]:
2082
2131
changed_content = True
2083
2132
elif kind[0] == 'file':
2084
if (self_entry.text_size != basis_entry.text_size
2085
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):
2086
2135
changed_content = True
2087
2136
elif kind[0] == 'symlink':
2088
2137
if self_entry.symlink_target != basis_entry.symlink_target:
2089
2138
changed_content = True
2090
2139
elif kind[0] == 'tree-reference':
2091
if (self_entry.reference_revision
2092
!= basis_entry.reference_revision):
2140
if (self_entry.reference_revision !=
2141
basis_entry.reference_revision):
2093
2142
changed_content = True
2094
2143
parent = (basis_parent, self_parent)
2095
2144
name = (basis_name, self_name)
2096
2145
executable = (basis_executable, self_executable)
2097
if (not changed_content and
2098
parent[0] == parent[1] and
2099
name[0] == name[1] and
2100
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]):
2101
2150
# Could happen when only the revision changed for a directory
2102
2151
# for instance.
2104
2153
yield (file_id, (path_in_source, path_in_target), changed_content,
2105
versioned, parent, name, kind, executable)
2154
versioned, parent, name, kind, executable)
2107
2156
def __len__(self):
2108
2157
"""Return the number of entries in the inventory."""
2111
2160
def _make_delta(self, old):
2112
2161
"""Make an inventory delta from two inventories."""
2113
if not isinstance(old, CHKInventory):
2162
if type(old) != CHKInventory:
2114
2163
return CommonInventory._make_delta(self, old)
2116
2165
for key, old_value, self_value in \
2117
self.id_to_entry.iter_changes(old.id_to_entry):
2166
self.id_to_entry.iter_changes(old.id_to_entry):
2118
2167
file_id = key[0]
2119
2168
if old_value is not None:
2120
2169
old_path = old.id2path(file_id)
2133
2182
def path2id(self, relpath):
2134
2183
"""See CommonInventory.path2id()."""
2135
2184
# TODO: perhaps support negative hits?
2136
if isinstance(relpath, (str, text_type)):
2137
names = osutils.splitpath(relpath)
2142
relpath = osutils.pathjoin(*relpath)
2143
2185
result = self._path_to_fileid_cache.get(relpath, None)
2144
2186
if result is not None:
2188
if isinstance(relpath, basestring):
2189
names = osutils.splitpath(relpath)
2146
2192
current_id = self.root_id
2147
2193
if current_id is None:
2173
2219
def to_lines(self):
2174
2220
"""Serialise the inventory to lines."""
2175
lines = [b"chkinventory:\n"]
2176
if self._search_key_name != b'plain':
2221
lines = ["chkinventory:\n"]
2222
if self._search_key_name != 'plain':
2177
2223
# custom ordering grouping things that don't change together
2178
lines.append(b'search_key_name: %s\n' % (
2179
self._search_key_name))
2180
lines.append(b"root_id: %s\n" % self.root_id)
2181
lines.append(b'parent_id_basename_to_file_id: %s\n' %
2182
(self.parent_id_basename_to_file_id.key()[0],))
2183
lines.append(b"revision_id: %s\n" % self.revision_id)
2184
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],))
2186
lines.append(b"revision_id: %s\n" % self.revision_id)
2187
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)
2188
2233
if self.parent_id_basename_to_file_id is not None:
2189
lines.append(b'parent_id_basename_to_file_id: %s\n' %
2190
(self.parent_id_basename_to_file_id.key()[0],))
2191
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],))
2195
2240
def root(self):
2196
2241
"""Get the root entry."""
2197
return self.get_entry(self.root_id)
2242
return self[self.root_id]
2200
2245
class CHKInventoryDirectory(InventoryDirectory):
2201
2246
"""A directory in an inventory."""
2203
__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',
2205
2253
def __init__(self, file_id, name, parent_id, chk_inventory):
2206
2254
# Don't call InventoryDirectory.__init__ - it isn't right for this
2208
2256
InventoryEntry.__init__(self, file_id, name, parent_id)
2209
2257
self._children = None
2258
self.kind = 'directory'
2210
2259
self._chk_inventory = chk_inventory
2224
2273
# No longer supported
2225
2274
if self._chk_inventory.parent_id_basename_to_file_id is None:
2226
2275
raise AssertionError("Inventories without"
2227
" parent_id_basename_to_file_id are no longer supported")
2276
" parent_id_basename_to_file_id are no longer supported")
2229
2278
# XXX: Todo - use proxy objects for the children rather than loading
2230
2279
# all when the attribute is referenced.
2231
2280
parent_id_index = self._chk_inventory.parent_id_basename_to_file_id
2232
2281
child_keys = set()
2233
2282
for (parent_id, name_utf8), file_id in parent_id_index.iteritems(
2234
key_filter=[StaticTuple(self.file_id,)]):
2283
key_filter=[StaticTuple(self.file_id,)]):
2235
2284
child_keys.add(StaticTuple(file_id,))
2237
2286
for file_id_key in child_keys:
2393
2443
entry = item[3]
2394
2444
if new_path is None and entry is not None:
2395
2445
raise errors.InconsistentDelta(item[0], item[1],
2396
"Entry with no new_path")
2446
"Entry with no new_path")
2397
2447
if new_path is not None and entry is None:
2398
2448
raise errors.InconsistentDelta(new_path, item[1],
2399
"new_path with no entry")
2449
"new_path with no entry")
2403
def mutable_inventory_from_tree(tree):
2404
"""Create a new inventory that has the same contents as a specified tree.
2406
:param tree: Revision tree to create inventory from
2408
entries = tree.iter_entries_by_dir()
2409
inv = Inventory(None, tree.get_revision_id())
2410
for path, inv_entry in entries:
2411
inv.add(inv_entry.copy())