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 (
57
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
60
57
class InventoryEntry(object):
90
87
>>> i = Inventory()
93
>>> i.add(InventoryDirectory(b'123', 'src', ROOT_ID))
90
>>> i.add(InventoryDirectory('123', 'src', ROOT_ID))
94
91
InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None)
95
>>> i.add(InventoryFile(b'2323', 'hello.c', parent_id='123'))
92
>>> i.add(InventoryFile('2323', 'hello.c', parent_id='123'))
96
93
InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None, revision=None)
97
94
>>> shouldbe = {0: '', 1: 'src', 2: 'src/hello.c'}
98
95
>>> for ix, j in enumerate(i.iter_entries()):
107
104
InventoryDirectory('2325', 'wibble', parent_id='123', revision=None)
108
105
>>> i.path2id('src/wibble')
110
109
>>> i.add(InventoryFile('2326', 'wibble.c', '2325'))
111
110
InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None, revision=None)
112
>>> i.get_entry('2326')
113
112
InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None, revision=None)
114
113
>>> for path, entry in i.iter_entries():
132
131
RENAMED = 'renamed'
133
132
MODIFIED_AND_RENAMED = 'modified and renamed'
135
__slots__ = ['file_id', 'revision', 'parent_id', 'name']
137
# Attributes that all InventoryEntry instances are expected to have, but
138
# that don't vary for all kinds of entry. (e.g. symlink_target is only
139
# relevant to InventoryLink, so there's no reason to make every
140
# InventoryFile instance allocate space to hold a value for it.)
141
# Attributes that only vary for files: executable, text_sha1, text_size,
147
# Attributes that only vary for symlinks: symlink_target
148
symlink_target = None
149
# Attributes that only vary for tree-references: reference_revision
150
reference_revision = None
153
136
def detect_changes(self, old_entry):
154
137
"""Return a (text_modified, meta_modified) from this to old_entry.
196
176
candidates[ie.revision] = ie
197
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)
199
189
def has_text(self):
200
190
"""Return true if the object this entry represents has textual data.
210
def __init__(self, file_id, name, parent_id):
200
def __init__(self, file_id, name, parent_id, text_id=None):
211
201
"""Create an InventoryEntry
213
203
The filename must be a single component, relative to the
214
204
parent directory; it cannot be a whole path or relative name.
216
>>> e = InventoryFile(b'123', 'hello.c', ROOT_ID)
206
>>> e = InventoryFile('123', 'hello.c', ROOT_ID)
221
>>> e = InventoryFile(b'123', 'src/hello.c', ROOT_ID)
211
>>> e = InventoryFile('123', 'src/hello.c', ROOT_ID)
222
212
Traceback (most recent call last):
223
213
InvalidEntryName: Invalid entry name: src/hello.c
215
if '/' in name or '\\' in name:
226
216
raise errors.InvalidEntryName(name=name)
227
if not isinstance(file_id, bytes):
228
raise TypeError(file_id)
217
self.executable = False
219
self.text_sha1 = None
220
self.text_size = None
229
221
self.file_id = file_id
223
self.text_id = text_id
232
224
self.parent_id = parent_id
225
self.symlink_target = None
226
self.reference_revision = None
234
228
def kind_character(self):
235
229
"""Return a short kind indicator useful for appending to names."""
236
raise errors.BzrError('unknown kind %r' % self.kind)
230
raise BzrError('unknown kind %r' % self.kind)
238
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())
241
261
def versionable_kind(kind):
242
262
return (kind in ('file', 'directory', 'symlink', 'tree-reference'))
400
class RootEntry(InventoryEntry):
402
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
403
'text_id', 'parent_id', 'children', 'executable',
404
'revision', 'symlink_target', 'reference_revision']
406
def _check(self, checker, rev_id):
407
"""See InventoryEntry._check"""
409
def __init__(self, file_id):
410
self.file_id = file_id
412
self.kind = 'directory'
413
self.parent_id = None
416
symbol_versioning.warn('RootEntry is deprecated as of bzr 0.10.'
417
' Please use InventoryDirectory instead.',
418
DeprecationWarning, stacklevel=2)
420
def __eq__(self, other):
421
if not isinstance(other, RootEntry):
422
return NotImplemented
424
return (self.file_id == other.file_id) \
425
and (self.children == other.children)
381
428
class InventoryDirectory(InventoryEntry):
382
429
"""A directory in an inventory."""
384
__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']
388
435
def _check(self, checker, rev_id):
389
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))
390
441
# In non rich root repositories we do not expect a file graph for the
392
443
if self.name == '' and not checker.rich_roots:
395
446
# to provide a per-fileid log. The hash of every directory content is
396
447
# "da..." below (the sha1sum of '').
397
448
checker.add_pending_item(rev_id,
398
('texts', self.file_id, self.revision), b'text',
399
b'da39a3ee5e6b4b0d3255bfef95601890afd80709')
449
('texts', self.file_id, self.revision), 'text',
450
'da39a3ee5e6b4b0d3255bfef95601890afd80709')
402
453
other = InventoryDirectory(self.file_id, self.name, self.parent_id)
408
459
def __init__(self, file_id, name, parent_id):
409
460
super(InventoryDirectory, self).__init__(file_id, name, parent_id)
410
461
self.children = {}
412
def sorted_children(self):
413
return sorted(viewitems(self.children))
462
self.kind = 'directory'
415
464
def kind_character(self):
416
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."""
420
482
class InventoryFile(InventoryEntry):
421
483
"""A file in an inventory."""
423
__slots__ = ['text_sha1', 'text_size', 'text_id', 'executable']
427
def __init__(self, file_id, name, parent_id):
428
super(InventoryFile, self).__init__(file_id, name, parent_id)
429
self.text_sha1 = None
430
self.text_size = None
432
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']
434
489
def _check(self, checker, tree_revision_id):
435
490
"""See InventoryEntry._check"""
436
491
# TODO: check size too.
437
492
checker.add_pending_item(tree_revision_id,
438
('texts', self.file_id, self.revision), b'text',
493
('texts', self.file_id, self.revision), 'text',
440
495
if self.text_size is None:
441
496
checker._report_items.append(
460
515
def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
461
516
output_to, reverse=False):
462
517
"""See InventoryEntry._diff."""
463
from breezy.diff import DiffText
518
from bzrlib.diff import DiffText
464
519
from_file_id = self.file_id
466
521
to_file_id = to_entry.file_id
467
to_path = to_tree.id2path(to_file_id)
469
523
to_file_id = None
471
if from_file_id is not None:
472
from_path = tree.id2path(from_file_id)
476
525
to_file_id, from_file_id = from_file_id, to_file_id
477
526
tree, to_tree = to_tree, tree
478
527
from_label, to_label = to_label, from_label
479
528
differ = DiffText(tree, to_tree, output_to, 'utf-8', '', '',
481
return differ.diff_text(from_path, to_path, from_label, to_label,
482
from_file_id, to_file_id)
530
return differ.diff_text(from_file_id, to_file_id, from_label, to_label)
484
532
def has_text(self):
485
533
"""See InventoryEntry.has_text."""
536
def __init__(self, file_id, name, parent_id):
537
super(InventoryFile, self).__init__(file_id, name, parent_id)
488
540
def kind_character(self):
489
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)
492
561
def _read_tree_state(self, path, work_tree):
493
562
"""See InventoryEntry._read_tree_state."""
494
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)
495
564
# FIXME: 20050930 probe for the text size when getting sha1
496
565
# in _read_tree_state
497
self.executable = work_tree.is_executable(path, self.file_id)
566
self.executable = work_tree.is_executable(self.file_id, path=path)
499
568
def __repr__(self):
500
569
return ("%s(%r, %r, parent_id=%r, sha1=%r, len=%s, revision=%s)"
526
595
class InventoryLink(InventoryEntry):
527
596
"""A file in an inventory."""
529
__slots__ = ['symlink_target']
533
def __init__(self, file_id, name, parent_id):
534
super(InventoryLink, self).__init__(file_id, name, parent_id)
535
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']
537
602
def _check(self, checker, tree_revision_id):
538
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))
539
608
if self.symlink_target is None:
540
609
checker._report_items.append(
541
610
'symlink {%s} has no target in revision {%s}'
542
611
% (self.file_id, tree_revision_id))
543
612
# Symlinks are stored as ''
544
613
checker.add_pending_item(tree_revision_id,
545
('texts', self.file_id, self.revision), b'text',
546
b'da39a3ee5e6b4b0d3255bfef95601890afd80709')
614
('texts', self.file_id, self.revision), 'text',
615
'da39a3ee5e6b4b0d3255bfef95601890afd80709')
549
618
other = InventoryLink(self.file_id, self.name, self.parent_id)
556
625
# FIXME: which _modified field should we use ? RBC 20051003
557
626
text_modified = (self.symlink_target != old_entry.symlink_target)
558
627
if text_modified:
559
trace.mutter(" symlink target changed")
628
mutter(" symlink target changed")
560
629
meta_modified = False
561
630
return text_modified, meta_modified
563
632
def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
564
633
output_to, reverse=False):
565
634
"""See InventoryEntry._diff."""
566
from breezy.diff import DiffSymlink
635
from bzrlib.diff import DiffSymlink
567
636
old_target = self.symlink_target
568
637
if to_entry is not None:
569
638
new_target = to_entry.symlink_target
579
648
differ = DiffSymlink(old_tree, new_tree, output_to)
580
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'
582
655
def kind_character(self):
583
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))
586
675
def _read_tree_state(self, path, work_tree):
587
676
"""See InventoryEntry._read_tree_state."""
588
self.symlink_target = work_tree.get_symlink_target(
589
work_tree.id2path(self.file_id), self.file_id)
677
self.symlink_target = work_tree.get_symlink_target(self.file_id)
591
679
def _forget_tree_state(self):
592
680
self.symlink_target = None
647
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)
650
753
def has_filename(self, filename):
651
754
return bool(self.path2id(filename))
654
757
"""Return as a string the path to file_id.
656
759
>>> i = Inventory()
657
>>> e = i.add(InventoryDirectory(b'src-id', 'src', ROOT_ID))
658
>>> e = i.add(InventoryFile(b'foo-id', 'foo.c', parent_id='src-id'))
659
>>> 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')
662
765
:raises NoSuchId: If file_id is not present in the inventory.
679
782
from_dir = self.root
680
783
yield '', self.root
681
elif isinstance(from_dir, bytes):
682
from_dir = self.get_entry(from_dir)
784
elif isinstance(from_dir, basestring):
785
from_dir = self[from_dir]
684
787
# unrolling the recursive called changed the time from
685
788
# 440ms/663ms (inline/total) to 116ms/116ms
686
children = sorted(viewitems(from_dir.children))
789
children = from_dir.children.items()
687
791
if not recursive:
688
792
for name, ie in children:
717
822
# if we finished all children, pop it off the stack
720
def _preload_cache(self):
721
"""Populate any caches, we are about to access all items.
723
The default implementation does nothing, because CommonInventory doesn't
728
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):
729
827
"""Iterate over the entries in a directory first order.
731
829
This returns all entries for a directory before returning
733
831
lexicographically sorted order, and is a hybrid between
734
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.
736
837
:return: This yields (path, entry) pairs
738
839
if specific_file_ids and not isinstance(specific_file_ids, set):
739
840
specific_file_ids = set(specific_file_ids)
740
841
# TODO? Perhaps this should return the from_dir so that the root is
741
842
# yielded? or maybe an option?
742
if from_dir is None and specific_file_ids is None:
743
# They are iterating from the root, and have not specified any
744
# specific entries to look at. All current callers fully consume the
745
# iterator, so we can safely assume we are accessing all entries
746
self._preload_cache()
747
843
if from_dir is None:
748
844
if self.root is None:
750
846
# Optimize a common case
751
if (specific_file_ids is not None and
847
if (not yield_parents and specific_file_ids is not None and
752
848
len(specific_file_ids) == 1):
753
849
file_id = list(specific_file_ids)[0]
754
if file_id is not None:
756
path = self.id2path(file_id)
757
except errors.NoSuchId:
760
yield path, self.get_entry(file_id)
851
yield self.id2path(file_id), self[file_id]
762
853
from_dir = self.root
763
if (specific_file_ids is None or
854
if (specific_file_ids is None or yield_parents or
764
855
self.root.file_id in specific_file_ids):
765
856
yield u'', self.root
766
elif isinstance(from_dir, bytes):
767
from_dir = self.get_entry(from_dir)
769
raise TypeError(from_dir)
857
elif isinstance(from_dir, basestring):
858
from_dir = self[from_dir]
771
860
if specific_file_ids is not None:
772
861
# TODO: jam 20070302 This could really be done as a loop rather
792
881
cur_relpath, cur_dir = stack.pop()
795
for child_name, child_ie in sorted(viewitems(cur_dir.children)):
884
for child_name, child_ie in sorted(cur_dir.children.iteritems()):
797
886
child_relpath = cur_relpath + child_name
799
888
if (specific_file_ids is None or
800
child_ie.file_id in specific_file_ids):
889
child_ie.file_id in specific_file_ids or
890
(yield_parents and child_ie.file_id in parents)):
801
891
yield child_relpath, child_ie
803
893
if child_ie.kind == 'directory':
816
906
for file_id in deletes:
817
907
delta.append((old.id2path(file_id), None, file_id, None))
818
908
for file_id in adds:
819
delta.append((None, self.id2path(file_id), file_id, self.get_entry(file_id)))
909
delta.append((None, self.id2path(file_id), file_id, self[file_id]))
820
910
for file_id in common:
821
if old.get_entry(file_id) != self.get_entry(file_id):
911
if old[file_id] != self[file_id]:
822
912
delta.append((old.id2path(file_id), self.id2path(file_id),
823
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)
826
924
def make_entry(self, kind, name, parent_id, file_id=None):
827
"""Simple thunk to breezy.bzr.inventory.make_entry."""
925
"""Simple thunk to bzrlib.inventory.make_entry."""
828
926
return make_entry(kind, name, parent_id, file_id)
830
928
def entries(self):
836
934
def descend(dir_ie, dir_path):
837
kids = sorted(viewitems(dir_ie.children))
935
kids = dir_ie.children.items()
838
937
for name, ie in kids:
839
938
child_path = osutils.pathjoin(dir_path, name)
840
939
accum.append((child_path, ie))
841
940
if ie.kind == 'directory':
842
941
descend(ie, child_path)
844
if self.root is not None:
845
descend(self.root, u'')
848
def get_entry_by_path(self, relpath):
849
"""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.
851
965
:param relpath: may be either a list of path components, or a single
852
966
string, in which case it is automatically split.
879
993
# or raise an error?
883
def path2id(self, relpath):
884
"""Walk down through directories to return entry of last component.
886
:param relpath: may be either a list of path components, or a single
887
string, in which case it is automatically split.
889
This returns the entry of the last component in the path,
890
which may be either a file or a directory.
892
Returns None IFF the path is not found.
894
ie = self.get_entry_by_path(relpath)
996
return parent.file_id
899
998
def filter(self, specific_fileids):
900
999
"""Get an inventory view filtered against a set of file-ids.
952
1051
returned quickly.
954
1053
>>> inv = Inventory()
955
>>> inv.add(InventoryFile(b'123-123', 'hello.c', ROOT_ID))
1054
>>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
956
1055
InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
957
>>> inv.get_entry(b'123-123').name
1056
>>> inv['123-123'].name
960
1059
Id's may be looked up from paths:
962
1061
>>> inv.path2id('hello.c')
964
>>> inv.has_id(b'123-123')
1063
>>> '123-123' in inv
967
1066
There are iterators over the contents:
1009
1108
applied the final inventory must be internally consistent, but it
1010
1109
is ok to supply changes which, if only half-applied would have an
1011
1110
invalid result - such as supplying two changes which rename two
1012
files, 'A' and 'B' with each other : [('A', 'B', b'A-id', a_entry),
1013
('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)].
1015
1114
Each change is a tuple, of the form (old_path, new_path, file_id,
1057
1156
for old_path, file_id in sorted(((op, f) for op, np, f, e in delta
1058
1157
if op is not None), reverse=True):
1059
1158
# Preserve unaltered children of file_id for later reinsertion.
1060
file_id_children = getattr(self.get_entry(file_id), 'children', {})
1159
file_id_children = getattr(self[file_id], 'children', {})
1061
1160
if len(file_id_children):
1062
1161
children[file_id] = file_id_children
1063
1162
if self.id2path(file_id) != old_path:
1137
1240
XXX: We may not want to merge this into bzr.dev.
1139
1242
if self.root is None:
1141
return iter(viewvalues(self._byid))
1244
for _, ie in self._byid.iteritems():
1143
1247
def __len__(self):
1144
1248
"""Returns number of entries."""
1145
1249
return len(self._byid)
1147
def get_entry(self, file_id):
1251
def __getitem__(self, file_id):
1148
1252
"""Return the entry for given file_id.
1150
1254
>>> inv = Inventory()
1151
>>> inv.add(InventoryFile(b'123123', 'hello.c', ROOT_ID))
1255
>>> inv.add(InventoryFile('123123', 'hello.c', ROOT_ID))
1152
1256
InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
1153
>>> inv.get_entry(b'123123').name
1257
>>> inv['123123'].name
1156
if not isinstance(file_id, bytes):
1157
raise TypeError(file_id)
1159
1261
return self._byid[file_id]
1160
1262
except KeyError:
1165
1267
return self._byid[file_id].kind
1167
1269
def get_child(self, parent_id, filename):
1168
return self.get_entry(parent_id).children.get(filename)
1270
return self[parent_id].children.get(filename)
1170
1272
def _add_child(self, entry):
1171
1273
"""Add an entry to the inventory, without adding it to its parent"""
1172
1274
if entry.file_id in self._byid:
1173
raise errors.BzrError(
1174
"inventory already contains entry with id {%s}" %
1275
raise BzrError("inventory already contains entry with id {%s}" %
1176
1277
self._byid[entry.file_id] = entry
1177
children = getattr(entry, 'children', {})
1178
if children is not None:
1179
for child in viewvalues(children):
1180
self._add_child(child)
1278
for child in getattr(entry, 'children', {}).itervalues():
1279
self._add_child(child)
1183
1282
def add(self, entry):
1184
1283
"""Add entry to inventory.
1285
To add a file to a branch ready to be committed, use Branch.add,
1188
1290
if entry.file_id in self._byid:
1227
1329
ie = make_entry(kind, parts[-1], parent_id, file_id)
1228
1330
return self.add(ie)
1230
def delete(self, file_id):
1332
def __delitem__(self, file_id):
1231
1333
"""Remove entry by id.
1233
1335
>>> inv = Inventory()
1234
>>> inv.add(InventoryFile(b'123', 'foo.c', ROOT_ID))
1336
>>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
1235
1337
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
1236
>>> inv.has_id(b'123')
1238
>>> inv.delete(b'123')
1239
>>> inv.has_id(b'123')
1242
ie = self.get_entry(file_id)
1243
1345
del self._byid[file_id]
1244
1346
if ie.parent_id is not None:
1245
del self.get_entry(ie.parent_id).children[ie.name]
1347
del self[ie.parent_id].children[ie.name]
1247
1349
def __eq__(self, other):
1248
1350
"""Compare two sets by comparing their contents.
1251
1353
>>> i2 = Inventory()
1254
>>> i1.add(InventoryFile(b'123', 'foo', ROOT_ID))
1356
>>> i1.add(InventoryFile('123', 'foo', ROOT_ID))
1255
1357
InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
1258
>>> i2.add(InventoryFile(b'123', 'foo', ROOT_ID))
1360
>>> i2.add(InventoryFile('123', 'foo', ROOT_ID))
1259
1361
InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
1287
1389
def _make_delta(self, old):
1288
1390
"""Make an inventory delta from two inventories."""
1289
old_getter = old.get_entry
1290
new_getter = self.get_entry
1291
old_ids = set(old.iter_all_ids())
1292
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)
1293
1395
adds = new_ids - old_ids
1294
1396
deletes = old_ids - new_ids
1295
1397
if not adds and not deletes:
1300
1402
for file_id in deletes:
1301
1403
delta.append((old.id2path(file_id), None, file_id, None))
1302
1404
for file_id in adds:
1303
delta.append((None, self.id2path(file_id), file_id, self.get_entry(file_id)))
1405
delta.append((None, self.id2path(file_id), file_id, self[file_id]))
1304
1406
for file_id in common:
1305
new_ie = new_getter(file_id)
1306
old_ie = old_getter(file_id)
1407
new_ie = new_getter[file_id]
1408
old_ie = old_getter[file_id]
1307
1409
# If xml_serializer returns the cached InventoryEntries (rather
1308
1410
# than always doing .copy()), inlining the 'is' check saves 2.7M
1309
1411
# calls to __eq__. Under lsprof this saves 20s => 6s.
1326
1428
ie = to_find_delete.pop()
1327
1429
to_delete.append(ie.file_id)
1328
1430
if ie.kind == 'directory':
1329
to_find_delete.extend(viewvalues(ie.children))
1431
to_find_delete.extend(ie.children.values())
1330
1432
for file_id in reversed(to_delete):
1331
ie = self.get_entry(file_id)
1332
1434
del self._byid[file_id]
1333
1435
if ie.parent_id is not None:
1334
del self.get_entry(ie.parent_id).children[ie.name]
1436
del self[ie.parent_id].children[ie.name]
1336
1438
self.root = None
1345
1447
new_name = ensure_normalized_name(new_name)
1346
1448
if not is_valid_name(new_name):
1347
raise errors.BzrError("not an acceptable filename: %r" % new_name)
1449
raise BzrError("not an acceptable filename: %r" % new_name)
1349
1451
new_parent = self._byid[new_parent_id]
1350
1452
if new_name in new_parent.children:
1351
raise errors.BzrError("%r already exists in %r" %
1352
(new_name, self.id2path(new_parent_id)))
1453
raise BzrError("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
1354
1455
new_parent_idpath = self.get_idpath(new_parent_id)
1355
1456
if file_id in new_parent_idpath:
1356
raise errors.BzrError(
1357
"cannot move directory %r into a subdirectory of itself, %r"
1457
raise BzrError("cannot move directory %r into a subdirectory of itself, %r"
1358
1458
% (self.id2path(file_id), self.id2path(new_parent_id)))
1360
1460
file_ie = self._byid[file_id]
1432
1531
if entry.parent_id is not None:
1433
1532
parent_str = entry.parent_id
1436
1535
name_str = entry.name.encode("utf8")
1437
1536
if entry.kind == 'file':
1438
1537
if entry.executable:
1442
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" % (
1443
1542
entry.file_id, parent_str, name_str, entry.revision,
1444
1543
entry.text_sha1, entry.text_size, exec_str)
1445
1544
elif entry.kind == 'directory':
1446
return b"dir: %s\n%s\n%s\n%s" % (
1545
return "dir: %s\n%s\n%s\n%s" % (
1447
1546
entry.file_id, parent_str, name_str, entry.revision)
1448
1547
elif entry.kind == 'symlink':
1449
return b"symlink: %s\n%s\n%s\n%s\n%s" % (
1548
return "symlink: %s\n%s\n%s\n%s\n%s" % (
1450
1549
entry.file_id, parent_str, name_str, entry.revision,
1451
1550
entry.symlink_target.encode("utf8"))
1452
1551
elif entry.kind == 'tree-reference':
1453
return b"tree: %s\n%s\n%s\n%s\n%s" % (
1552
return "tree: %s\n%s\n%s\n%s\n%s" % (
1454
1553
entry.file_id, parent_str, name_str, entry.revision,
1455
1554
entry.reference_revision)
1518
1617
keys = [StaticTuple(f,).intern() for f in directories_to_expand]
1519
1618
directories_to_expand = set()
1520
1619
items = self.parent_id_basename_to_file_id.iteritems(keys)
1521
next_file_ids = {item[1] for item in items}
1620
next_file_ids = set([item[1] for item in items])
1522
1621
next_file_ids = next_file_ids.difference(interesting)
1523
1622
interesting.update(next_file_ids)
1524
1623
for entry in self._getitems(next_file_ids):
1525
1624
if entry.kind == 'directory':
1526
1625
directories_to_expand.add(entry.file_id)
1527
children_of_parent_id.setdefault(entry.parent_id, set()
1528
).add(entry.file_id)
1626
children_of_parent_id.setdefault(entry.parent_id, []
1627
).append(entry.file_id)
1529
1628
return interesting, children_of_parent_id
1531
1630
def filter(self, specific_fileids):
1571
def _bytes_to_utf8name_key(data):
1572
"""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."""
1573
1676
# We don't normally care about name, except for times when we want
1574
1677
# to filter out empty names because of non rich-root...
1575
sections = data.split(b'\n')
1576
kind, file_id = sections[0].split(b': ')
1577
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]))
1579
1682
def _bytes_to_entry(self, bytes):
1580
1683
"""Deserialise a serialised entry."""
1581
sections = bytes.split(b'\n')
1582
if sections[0].startswith(b"file: "):
1684
sections = bytes.split('\n')
1685
if sections[0].startswith("file: "):
1583
1686
result = InventoryFile(sections[0][6:],
1584
1687
sections[2].decode('utf8'),
1586
1689
result.text_sha1 = sections[4]
1587
1690
result.text_size = int(sections[5])
1588
result.executable = sections[6] == b"Y"
1589
elif sections[0].startswith(b"dir: "):
1691
result.executable = sections[6] == "Y"
1692
elif sections[0].startswith("dir: "):
1590
1693
result = CHKInventoryDirectory(sections[0][5:],
1591
1694
sections[2].decode('utf8'),
1592
1695
sections[1], self)
1593
elif sections[0].startswith(b"symlink: "):
1696
elif sections[0].startswith("symlink: "):
1594
1697
result = InventoryLink(sections[0][9:],
1595
1698
sections[2].decode('utf8'),
1597
1700
result.symlink_target = sections[4].decode('utf8')
1598
elif sections[0].startswith(b"tree: "):
1701
elif sections[0].startswith("tree: "):
1599
1702
result = TreeReference(sections[0][6:],
1600
1703
sections[2].decode('utf8'),
1602
1705
result.reference_revision = sections[4]
1604
1707
raise ValueError("Not a serialised entry %r" % bytes)
1605
result.file_id = bytesintern(result.file_id)
1606
result.revision = bytesintern(sections[3])
1607
if result.parent_id == b'':
1708
result.file_id = intern(result.file_id)
1709
result.revision = intern(sections[3])
1710
if result.parent_id == '':
1608
1711
result.parent_id = None
1609
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())
1612
1723
def create_by_apply_delta(self, inventory_delta, new_revision_id,
1613
1724
propagate_caches=False):
1614
1725
"""Create a new CHKInventory by applying inventory_delta to this one.
1741
1852
new_key, [None, None])[1] = new_value
1742
1853
# validate that deletes are complete.
1743
1854
for file_id in deletes:
1744
entry = self.get_entry(file_id)
1855
entry = self[file_id]
1745
1856
if entry.kind != 'directory':
1747
1858
# This loop could potentially be better by using the id_basename
1748
1859
# map to just get the child file ids.
1749
for child in viewvalues(entry.children):
1860
for child in entry.children.values():
1750
1861
if child.file_id not in altered:
1751
1862
raise errors.InconsistentDelta(self.id2path(child.file_id),
1752
1863
child.file_id, "Child not deleted or reparented when "
1789
1900
:return: A CHKInventory
1791
lines = bytes.split(b'\n')
1792
if lines[-1] != b'':
1902
lines = bytes.split('\n')
1793
1904
raise AssertionError('bytes to deserialize must end with an eol')
1795
if lines[0] != b'chkinventory:':
1906
if lines[0] != 'chkinventory:':
1796
1907
raise ValueError("not a serialised CHKInventory: %r" % bytes)
1798
allowed_keys = frozenset((b'root_id', b'revision_id',
1799
b'parent_id_basename_to_file_id',
1800
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',
1801
1912
for line in lines[1:]:
1802
key, value = line.split(b': ', 1)
1913
key, value = line.split(': ', 1)
1803
1914
if key not in allowed_keys:
1804
1915
raise errors.BzrError('Unknown key in inventory: %r\n%r'
1805
1916
% (key, bytes))
1807
1918
raise errors.BzrError('Duplicate key in inventory: %r\n%r'
1808
1919
% (key, bytes))
1809
1920
info[key] = value
1810
revision_id = bytesintern(info[b'revision_id'])
1811
root_id = bytesintern(info[b'root_id'])
1812
search_key_name = bytesintern(info.get(b'search_key_name', b'plain'))
1813
parent_id_basename_to_file_id = bytesintern(info.get(
1814
b'parent_id_basename_to_file_id', None))
1815
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:'):
1816
1927
raise ValueError('parent_id_basename_to_file_id should be a sha1'
1817
1928
' key not %r' % (parent_id_basename_to_file_id,))
1818
id_to_entry = info[b'id_to_entry']
1819
if not id_to_entry.startswith(b'sha1:'):
1929
id_to_entry = info['id_to_entry']
1930
if not id_to_entry.startswith('sha1:'):
1820
1931
raise ValueError('id_to_entry should be a sha1'
1821
1932
' key not %r' % (id_to_entry,))
1904
2015
return self._bytes_to_entry(
1905
next(self.id_to_entry.iteritems([StaticTuple(file_id,)]))[1])
2016
self.id_to_entry.iteritems([StaticTuple(file_id,)]).next()[1])
1906
2017
except StopIteration:
1907
2018
# really we're passing an inventory, not a tree...
1908
2019
raise errors.NoSuchId(self, file_id)
1910
2021
def _getitems(self, file_ids):
1911
"""Similar to get_entry, but lets you query for multiple.
2022
"""Similar to __getitem__, but lets you query for multiple.
1913
2024
The returned order is undefined. And currently if an item doesn't
1914
2025
exist, it isn't included in the output.
1942
2053
"""Yield the parents of file_id up to the root."""
1943
2054
while file_id is not None:
1945
ie = self.get_entry(file_id)
1946
2057
except KeyError:
1947
2058
raise errors.NoSuchId(tree=self, file_id=file_id)
1949
2060
file_id = ie.parent_id
1951
def iter_all_ids(self):
1952
2063
"""Iterate over all file-ids."""
1953
2064
for key, _ in self.id_to_entry.iteritems():
1956
2067
def iter_just_entries(self):
1957
2068
"""Iterate over all entries.
1959
2070
Unlike iter_entries(), just the entries are returned (not (path, ie))
1960
2071
and the order of entries is undefined.
1969
2080
self._fileid_to_entry_cache[file_id] = ie
1972
def _preload_cache(self):
1973
"""Make sure all file-ids are in _fileid_to_entry_cache"""
1974
if self._fully_cached:
1975
return # No need to do it again
1976
# The optimal sort order is to use iteritems() directly
1977
cache = self._fileid_to_entry_cache
1978
for key, entry in self.id_to_entry.iteritems():
1980
if file_id not in cache:
1981
ie = self._bytes_to_entry(entry)
1985
last_parent_id = last_parent_ie = None
1986
pid_items = self.parent_id_basename_to_file_id.iteritems()
1987
for key, child_file_id in pid_items:
1988
if key == (b'', b''): # This is the root
1989
if child_file_id != self.root_id:
1990
raise ValueError('Data inconsistency detected.'
1991
' We expected data with key ("","") to match'
1992
' the root id, but %s != %s'
1993
% (child_file_id, self.root_id))
1995
parent_id, basename = key
1996
ie = cache[child_file_id]
1997
if parent_id == last_parent_id:
1998
parent_ie = last_parent_ie
2000
parent_ie = cache[parent_id]
2001
if parent_ie.kind != 'directory':
2002
raise ValueError('Data inconsistency detected.'
2003
' An entry in the parent_id_basename_to_file_id map'
2004
' has parent_id {%s} but the kind of that object'
2005
' is %r not "directory"' % (parent_id, parent_ie.kind))
2006
if parent_ie._children is None:
2007
parent_ie._children = {}
2008
basename = basename.decode('utf-8')
2009
if basename in parent_ie._children:
2010
existing_ie = parent_ie._children[basename]
2011
if existing_ie != ie:
2012
raise ValueError('Data inconsistency detected.'
2013
' Two entries with basename %r were found'
2014
' in the parent entry {%s}'
2015
% (basename, parent_id))
2016
if basename != ie.name:
2017
raise ValueError('Data inconsistency detected.'
2018
' In the parent_id_basename_to_file_id map, file_id'
2019
' {%s} is listed as having basename %r, but in the'
2020
' id_to_entry map it is %r'
2021
% (child_file_id, basename, ie.name))
2022
parent_ie._children[basename] = ie
2023
self._fully_cached = True
2025
2083
def iter_changes(self, basis):
2026
2084
"""Generate a Tree.iter_changes change list between this and basis.
2124
2182
def path2id(self, relpath):
2125
2183
"""See CommonInventory.path2id()."""
2126
2184
# TODO: perhaps support negative hits?
2127
if isinstance(relpath, (str, text_type)):
2128
names = osutils.splitpath(relpath)
2133
relpath = osutils.pathjoin(*relpath)
2134
2185
result = self._path_to_fileid_cache.get(relpath, None)
2135
2186
if result is not None:
2188
if isinstance(relpath, basestring):
2189
names = osutils.splitpath(relpath)
2137
2192
current_id = self.root_id
2138
2193
if current_id is None:
2164
2219
def to_lines(self):
2165
2220
"""Serialise the inventory to lines."""
2166
lines = [b"chkinventory:\n"]
2167
if self._search_key_name != b'plain':
2221
lines = ["chkinventory:\n"]
2222
if self._search_key_name != 'plain':
2168
2223
# custom ordering grouping things that don't change together
2169
lines.append(b'search_key_name: %s\n' % (
2170
self._search_key_name))
2171
lines.append(b"root_id: %s\n" % self.root_id)
2172
lines.append(b'parent_id_basename_to_file_id: %s\n' %
2224
lines.append('search_key_name: %s\n' % (self._search_key_name,))
2225
lines.append("root_id: %s\n" % self.root_id)
2226
lines.append('parent_id_basename_to_file_id: %s\n' %
2173
2227
(self.parent_id_basename_to_file_id.key()[0],))
2174
lines.append(b"revision_id: %s\n" % self.revision_id)
2175
lines.append(b"id_to_entry: %s\n" % (self.id_to_entry.key()[0],))
2228
lines.append("revision_id: %s\n" % self.revision_id)
2229
lines.append("id_to_entry: %s\n" % (self.id_to_entry.key()[0],))
2177
lines.append(b"revision_id: %s\n" % self.revision_id)
2178
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)
2179
2233
if self.parent_id_basename_to_file_id is not None:
2180
lines.append(b'parent_id_basename_to_file_id: %s\n' %
2234
lines.append('parent_id_basename_to_file_id: %s\n' %
2181
2235
(self.parent_id_basename_to_file_id.key()[0],))
2182
lines.append(b"id_to_entry: %s\n" % (self.id_to_entry.key()[0],))
2236
lines.append("id_to_entry: %s\n" % (self.id_to_entry.key()[0],))
2186
2240
def root(self):
2187
2241
"""Get the root entry."""
2188
return self.get_entry(self.root_id)
2242
return self[self.root_id]
2191
2245
class CHKInventoryDirectory(InventoryDirectory):
2192
2246
"""A directory in an inventory."""
2194
__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',
2196
2253
def __init__(self, file_id, name, parent_id, chk_inventory):
2197
2254
# Don't call InventoryDirectory.__init__ - it isn't right for this
2199
2256
InventoryEntry.__init__(self, file_id, name, parent_id)
2200
2257
self._children = None
2258
self.kind = 'directory'
2201
2259
self._chk_inventory = chk_inventory
2386
2448
raise errors.InconsistentDelta(new_path, item[1],
2387
2449
"new_path with no entry")
2391
def mutable_inventory_from_tree(tree):
2392
"""Create a new inventory that has the same contents as a specified tree.
2394
:param tree: Revision tree to create inventory from
2396
entries = tree.iter_entries_by_dir()
2397
inv = Inventory(None, tree.get_revision_id())
2398
for path, inv_entry in entries:
2399
inv.add(inv_entry.copy())