118
88
src/wibble/wibble.c
119
89
>>> i.id2path('2326')
120
90
'src/wibble/wibble.c'
92
:todo: Maybe also keep the full path of the entry, and the children?
93
But those depend on its position within a particular inventory, and
94
it would be nice not to need to hold the backpointer here.
123
# Constants returned by describe_change()
125
# TODO: These should probably move to some kind of FileChangeDescription
126
# class; that's like what's inside a TreeDelta but we want to be able to
127
# generate them just for one file at a time.
129
MODIFIED_AND_RENAMED = 'modified and renamed'
131
__slots__ = ['file_id', 'revision', 'parent_id', 'name']
133
# Attributes that all InventoryEntry instances are expected to have, but
134
# that don't vary for all kinds of entry. (e.g. symlink_target is only
135
# relevant to InventoryLink, so there's no reason to make every
136
# InventoryFile instance allocate space to hold a value for it.)
137
# Attributes that only vary for files: executable, text_sha1, text_size,
143
# Attributes that only vary for symlinks: symlink_target
144
symlink_target = None
145
# Attributes that only vary for tree-references: reference_revision
146
reference_revision = None
149
def detect_changes(self, old_entry):
150
"""Return a (text_modified, meta_modified) from this to old_entry.
152
_read_tree_state must have been called on self and old_entry prior to
153
calling detect_changes.
157
def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
158
output_to, reverse=False):
159
"""Perform a diff between two entries of the same kind."""
161
def parent_candidates(self, previous_inventories):
162
"""Find possible per-file graph parents.
164
This is currently defined by:
165
- Select the last changed revision in the parent inventory.
166
- Do deal with a short lived bug in bzr 0.8's development two entries
167
that have the same last changed but different 'x' bit settings are
170
# revision:ie mapping for each ie found in previous_inventories.
172
# identify candidate head revision ids.
173
for inv in previous_inventories:
174
if self.file_id in inv:
175
ie = inv[self.file_id]
176
if ie.revision in candidates:
177
# same revision value in two different inventories:
178
# correct possible inconsistencies:
179
# * there was a bug in revision updates with 'x' bit
182
if candidates[ie.revision].executable != ie.executable:
183
candidates[ie.revision].executable = False
184
ie.executable = False
185
except AttributeError:
188
# add this revision as a candidate.
189
candidates[ie.revision] = ie
193
"""Return true if the object this entry represents has textual data.
195
Note that textual data includes binary content.
197
Also note that all entries get weave files created for them.
198
This attribute is primarily used when upgrading from old trees that
199
did not have the weave index for all inventory entries.
203
def __init__(self, file_id, name, parent_id):
96
def __init__(self, file_id, name, kind='file', text_id=None,
204
98
"""Create an InventoryEntry
206
100
The filename must be a single component, relative to the
207
101
parent directory; it cannot be a whole path or relative name.
209
>>> e = InventoryFile('123', 'hello.c', ROOT_ID)
103
>>> e = InventoryEntry('123', 'hello.c')
214
>>> e = InventoryFile('123', 'src/hello.c', ROOT_ID)
108
>>> e = InventoryEntry('123', 'src/hello.c')
215
109
Traceback (most recent call last):
216
InvalidEntryName: Invalid entry name: src/hello.c
110
BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", [])
218
if '/' in name or '\\' in name:
219
raise errors.InvalidEntryName(name=name)
113
if len(splitpath(name)) != 1:
114
bailout('InventoryEntry name is not a simple filename: %r'
220
117
self.file_id = file_id
119
assert kind in ['file', 'directory']
121
self.text_id = text_id
223
122
self.parent_id = parent_id
225
def kind_character(self):
226
"""Return a short kind indicator useful for appending to names."""
227
raise BzrError('unknown kind %r' % self.kind)
229
known_kinds = ('file', 'directory', 'symlink')
231
def _put_in_tar(self, item, tree):
232
"""populate item for stashing in a tar, and return the content stream.
234
If no content is available, return None.
236
raise BzrError("don't know how to export {%s} of kind %r" %
237
(self.file_id, self.kind))
239
def _put_on_disk(self, fullpath, tree):
240
"""Put this entry onto disk at fullpath, from tree tree."""
241
raise BzrError("don't know how to export {%s} of kind %r" % (self.file_id, self.kind))
243
def sorted_children(self):
244
return sorted(self.children.items())
247
def versionable_kind(kind):
248
return (kind in ('file', 'directory', 'symlink', 'tree-reference'))
250
def check(self, checker, rev_id, inv):
251
"""Check this inventory entry is intact.
253
This is a template method, override _check for kind specific
256
:param checker: Check object providing context for the checks;
257
can be used to find out what parts of the repository have already
259
:param rev_id: Revision id from which this InventoryEntry was loaded.
260
Not necessarily the last-changed revision for this file.
261
:param inv: Inventory from which the entry was loaded.
263
if self.parent_id is not None:
264
if not inv.has_id(self.parent_id):
265
raise BzrCheckError('missing parent {%s} in inventory for revision {%s}'
266
% (self.parent_id, rev_id))
267
checker._add_entry_to_text_key_references(inv, self)
268
self._check(checker, rev_id)
270
def _check(self, checker, rev_id):
271
"""Check this inventory entry for kind specific errors."""
272
checker._report_items.append(
273
'unknown entry kind %r in revision {%s}' % (self.kind, rev_id))
276
"""Clone this inventory entry."""
277
raise NotImplementedError
280
def describe_change(old_entry, new_entry):
281
"""Describe the change between old_entry and this.
283
This smells of being an InterInventoryEntry situation, but as its
284
the first one, we're making it a static method for now.
286
An entry with a different parent, or different name is considered
287
to be renamed. Reparenting is an internal detail.
288
Note that renaming the parent does not trigger a rename for the
291
# TODO: Perhaps return an object rather than just a string
292
if old_entry is new_entry:
293
# also the case of both being None
295
elif old_entry is None:
297
elif new_entry is None:
299
if old_entry.kind != new_entry.kind:
301
text_modified, meta_modified = new_entry.detect_changes(old_entry)
302
if text_modified or meta_modified:
306
# TODO 20060511 (mbp, rbc) factor out 'detect_rename' here.
307
if old_entry.parent_id != new_entry.parent_id:
309
elif old_entry.name != new_entry.name:
313
if renamed and not modified:
314
return InventoryEntry.RENAMED
315
if modified and not renamed:
317
if modified and renamed:
318
return InventoryEntry.MODIFIED_AND_RENAMED
322
return ("%s(%r, %r, parent_id=%r, revision=%r)"
323
% (self.__class__.__name__,
329
def __eq__(self, other):
331
# For the case when objects are cached
333
if not isinstance(other, InventoryEntry):
334
return NotImplemented
336
return ((self.file_id == other.file_id)
337
and (self.name == other.name)
338
and (other.symlink_target == self.symlink_target)
339
and (self.text_sha1 == other.text_sha1)
340
and (self.text_size == other.text_size)
341
and (self.text_id == other.text_id)
342
and (self.parent_id == other.parent_id)
343
and (self.kind == other.kind)
344
and (self.revision == other.revision)
345
and (self.executable == other.executable)
346
and (self.reference_revision == other.reference_revision)
349
def __ne__(self, other):
350
return not (self == other)
353
raise ValueError('not hashable')
355
def _unchanged(self, previous_ie):
356
"""Has this entry changed relative to previous_ie.
358
This method should be overridden in child classes.
361
# different inv parent
362
if previous_ie.parent_id != self.parent_id:
365
elif previous_ie.name != self.name:
367
elif previous_ie.kind != self.kind:
371
def _read_tree_state(self, path, work_tree):
372
"""Populate fields in the inventory entry from the given tree.
374
Note that this should be modified to be a noop on virtual trees
375
as all entries created there are prepopulated.
377
# TODO: Rather than running this manually, we should check the
378
# working sha1 and other expensive properties when they're
379
# first requested, or preload them if they're already known
380
pass # nothing to do by default
382
def _forget_tree_state(self):
386
class InventoryDirectory(InventoryEntry):
387
"""A directory in an inventory."""
389
__slots__ = ['children']
393
def _check(self, checker, rev_id):
394
"""See InventoryEntry._check"""
395
# In non rich root repositories we do not expect a file graph for the
397
if self.name == '' and not checker.rich_roots:
399
# Directories are stored as an empty file, but the file should exist
400
# to provide a per-fileid log. The hash of every directory content is
401
# "da..." below (the sha1sum of '').
402
checker.add_pending_item(rev_id,
403
('texts', self.file_id, self.revision), 'text',
404
'da39a3ee5e6b4b0d3255bfef95601890afd80709')
407
other = InventoryDirectory(self.file_id, self.name, self.parent_id)
408
other.revision = self.revision
409
# note that children are *not* copied; they're pulled across when
413
def __init__(self, file_id, name, parent_id):
414
super(InventoryDirectory, self).__init__(file_id, name, parent_id)
417
def kind_character(self):
418
"""See InventoryEntry.kind_character."""
421
def _put_in_tar(self, item, tree):
422
"""See InventoryEntry._put_in_tar."""
423
item.type = tarfile.DIRTYPE
430
def _put_on_disk(self, fullpath, tree):
431
"""See InventoryEntry._put_on_disk."""
435
class InventoryFile(InventoryEntry):
436
"""A file in an inventory."""
438
__slots__ = ['text_sha1', 'text_size', 'text_id', 'executable']
442
def __init__(self, file_id, name, parent_id):
443
super(InventoryFile, self).__init__(file_id, name, parent_id)
444
123
self.text_sha1 = None
445
124
self.text_size = None
447
self.executable = False
449
def _check(self, checker, tree_revision_id):
450
"""See InventoryEntry._check"""
451
# TODO: check size too.
452
checker.add_pending_item(tree_revision_id,
453
('texts', self.file_id, self.revision), 'text',
455
if self.text_size is None:
456
checker._report_items.append(
457
'fileid {%s} in {%s} has None for text_size' % (self.file_id,
461
other = InventoryFile(self.file_id, self.name, self.parent_id)
462
other.executable = self.executable
463
other.text_id = self.text_id
128
other = InventoryEntry(self.file_id, self.name, self.kind,
129
self.text_id, self.parent_id)
464
130
other.text_sha1 = self.text_sha1
465
131
other.text_size = self.text_size
466
other.revision = self.revision
469
def detect_changes(self, old_entry):
470
"""See InventoryEntry.detect_changes."""
471
text_modified = (self.text_sha1 != old_entry.text_sha1)
472
meta_modified = (self.executable != old_entry.executable)
473
return text_modified, meta_modified
475
def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
476
output_to, reverse=False):
477
"""See InventoryEntry._diff."""
478
from bzrlib.diff import DiffText
479
from_file_id = self.file_id
481
to_file_id = to_entry.file_id
485
to_file_id, from_file_id = from_file_id, to_file_id
486
tree, to_tree = to_tree, tree
487
from_label, to_label = to_label, from_label
488
differ = DiffText(tree, to_tree, output_to, 'utf-8', '', '',
490
return differ.diff_text(from_file_id, to_file_id, from_label, to_label)
493
"""See InventoryEntry.has_text."""
496
def kind_character(self):
497
"""See InventoryEntry.kind_character."""
500
def _put_in_tar(self, item, tree):
501
"""See InventoryEntry._put_in_tar."""
502
item.type = tarfile.REGTYPE
503
fileobj = tree.get_file(self.file_id)
504
item.size = self.text_size
505
if tree.is_executable(self.file_id):
511
def _put_on_disk(self, fullpath, tree):
512
"""See InventoryEntry._put_on_disk."""
513
osutils.pumpfile(tree.get_file(self.file_id), file(fullpath, 'wb'))
514
if tree.is_executable(self.file_id):
515
os.chmod(fullpath, 0755)
517
def _read_tree_state(self, path, work_tree):
518
"""See InventoryEntry._read_tree_state."""
519
self.text_sha1 = work_tree.get_file_sha1(self.file_id, path=path)
520
# FIXME: 20050930 probe for the text size when getting sha1
521
# in _read_tree_state
522
self.executable = work_tree.is_executable(self.file_id, path=path)
524
135
def __repr__(self):
525
return ("%s(%r, %r, parent_id=%r, sha1=%r, len=%s, revision=%s)"
136
return ("%s(%r, %r, kind=%r, parent_id=%r)"
526
137
% (self.__class__.__name__,
534
def _forget_tree_state(self):
535
self.text_sha1 = None
537
def _unchanged(self, previous_ie):
538
"""See InventoryEntry._unchanged."""
539
compatible = super(InventoryFile, self)._unchanged(previous_ie)
540
if self.text_sha1 != previous_ie.text_sha1:
543
# FIXME: 20050930 probe for the text size when getting sha1
544
# in _read_tree_state
545
self.text_size = previous_ie.text_size
546
if self.executable != previous_ie.executable:
551
class InventoryLink(InventoryEntry):
552
"""A file in an inventory."""
554
__slots__ = ['symlink_target']
558
def __init__(self, file_id, name, parent_id):
559
super(InventoryLink, self).__init__(file_id, name, parent_id)
560
self.symlink_target = None
562
def _check(self, checker, tree_revision_id):
563
"""See InventoryEntry._check"""
564
if self.symlink_target is None:
565
checker._report_items.append(
566
'symlink {%s} has no target in revision {%s}'
567
% (self.file_id, tree_revision_id))
568
# Symlinks are stored as ''
569
checker.add_pending_item(tree_revision_id,
570
('texts', self.file_id, self.revision), 'text',
571
'da39a3ee5e6b4b0d3255bfef95601890afd80709')
574
other = InventoryLink(self.file_id, self.name, self.parent_id)
575
other.symlink_target = self.symlink_target
576
other.revision = self.revision
579
def detect_changes(self, old_entry):
580
"""See InventoryEntry.detect_changes."""
581
# FIXME: which _modified field should we use ? RBC 20051003
582
text_modified = (self.symlink_target != old_entry.symlink_target)
584
mutter(" symlink target changed")
585
meta_modified = False
586
return text_modified, meta_modified
588
def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
589
output_to, reverse=False):
590
"""See InventoryEntry._diff."""
591
from bzrlib.diff import DiffSymlink
592
old_target = self.symlink_target
593
if to_entry is not None:
594
new_target = to_entry.symlink_target
603
new_target, old_target = old_target, new_target
604
differ = DiffSymlink(old_tree, new_tree, output_to)
605
return differ.diff_symlink(old_target, new_target)
607
def kind_character(self):
608
"""See InventoryEntry.kind_character."""
611
def _put_in_tar(self, item, tree):
612
"""See InventoryEntry._put_in_tar."""
613
item.type = tarfile.SYMTYPE
617
item.linkname = self.symlink_target
620
def _put_on_disk(self, fullpath, tree):
621
"""See InventoryEntry._put_on_disk."""
623
os.symlink(self.symlink_target, fullpath)
625
raise BzrError("Failed to create symlink %r -> %r, error: %s" % (fullpath, self.symlink_target, e))
627
def _read_tree_state(self, path, work_tree):
628
"""See InventoryEntry._read_tree_state."""
629
self.symlink_target = work_tree.get_symlink_target(self.file_id)
631
def _forget_tree_state(self):
632
self.symlink_target = None
634
def _unchanged(self, previous_ie):
635
"""See InventoryEntry._unchanged."""
636
compatible = super(InventoryLink, self)._unchanged(previous_ie)
637
if self.symlink_target != previous_ie.symlink_target:
642
class TreeReference(InventoryEntry):
644
__slots__ = ['reference_revision']
646
kind = 'tree-reference'
648
def __init__(self, file_id, name, parent_id, revision=None,
649
reference_revision=None):
650
InventoryEntry.__init__(self, file_id, name, parent_id)
651
self.revision = revision
652
self.reference_revision = reference_revision
655
return TreeReference(self.file_id, self.name, self.parent_id,
656
self.revision, self.reference_revision)
658
def _read_tree_state(self, path, work_tree):
659
"""Populate fields in the inventory entry from the given tree.
661
self.reference_revision = work_tree.get_reference_revision(
664
def _forget_tree_state(self):
665
self.reference_revision = None
667
def _unchanged(self, previous_ie):
668
"""See InventoryEntry._unchanged."""
669
compatible = super(TreeReference, self)._unchanged(previous_ie)
670
if self.reference_revision != previous_ie.reference_revision:
675
class CommonInventory(object):
676
"""Basic inventory logic, defined in terms of primitives like has_id.
678
An inventory is the metadata about the contents of a tree.
680
This is broadly a map from file_id to entries such as directories, files,
681
symlinks and tree references. Each entry maintains its own metadata like
682
SHA1 and length for files, or children for a directory.
684
Entries can be looked up either by path or by file_id.
686
InventoryEntry objects must not be modified after they are
687
inserted, other than through the Inventory API.
690
def __contains__(self, file_id):
691
"""True if this entry contains a file with given id.
693
>>> inv = Inventory()
694
>>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
695
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
701
Note that this method along with __iter__ are not encouraged for use as
702
they are less clear than specific query methods - they may be rmeoved
705
return self.has_id(file_id)
707
def has_filename(self, filename):
708
return bool(self.path2id(filename))
710
def id2path(self, file_id):
711
"""Return as a string the path to file_id.
714
>>> e = i.add(InventoryDirectory('src-id', 'src', ROOT_ID))
715
>>> e = i.add(InventoryFile('foo-id', 'foo.c', parent_id='src-id'))
716
>>> print i.id2path('foo-id')
719
:raises NoSuchId: If file_id is not present in the inventory.
721
# get all names, skipping root
722
return '/'.join(reversed(
723
[parent.name for parent in
724
self._iter_file_id_parents(file_id)][:-1]))
726
def iter_entries(self, from_dir=None, recursive=True):
727
"""Return (path, entry) pairs, in order by name.
144
def to_element(self):
145
"""Convert to XML element"""
148
e.set('name', self.name)
149
e.set('file_id', self.file_id)
150
e.set('kind', self.kind)
152
if self.text_size is not None:
153
e.set('text_size', '%d' % self.text_size)
155
for f in ['text_id', 'text_sha1', 'parent_id']:
165
def from_element(cls, elt):
166
assert elt.tag == 'entry'
167
self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind'))
168
self.text_id = elt.get('text_id')
169
self.text_sha1 = elt.get('text_sha1')
170
self.parent_id = elt.get('parent_id')
729
:param from_dir: if None, start from the root,
730
otherwise start from this directory (either file-id or entry)
731
:param recursive: recurse into directories or not
734
if self.root is None:
738
elif isinstance(from_dir, basestring):
739
from_dir = self[from_dir]
741
# unrolling the recursive called changed the time from
742
# 440ms/663ms (inline/total) to 116ms/116ms
743
children = from_dir.children.items()
746
for name, ie in children:
749
children = collections.deque(children)
750
stack = [(u'', children)]
752
from_dir_relpath, children = stack[-1]
755
name, ie = children.popleft()
757
# we know that from_dir_relpath never ends in a slash
758
# and 'f' doesn't begin with one, we can do a string op, rather
759
# than the checks of pathjoin(), though this means that all paths
761
path = from_dir_relpath + '/' + name
765
if ie.kind != 'directory':
768
# But do this child first
769
new_children = ie.children.items()
771
new_children = collections.deque(new_children)
772
stack.append((path, new_children))
773
# Break out of inner loop, so that we start outer loop with child
776
# if we finished all children, pop it off the stack
779
def iter_entries_by_dir(self, from_dir=None, specific_file_ids=None,
780
yield_parents=False):
781
"""Iterate over the entries in a directory first order.
783
This returns all entries for a directory before returning
784
the entries for children of a directory. This is not
785
lexicographically sorted order, and is a hybrid between
786
depth-first and breadth-first.
788
:param yield_parents: If True, yield the parents from the root leading
789
down to specific_file_ids that have been requested. This has no
790
impact if specific_file_ids is None.
791
:return: This yields (path, entry) pairs
793
if specific_file_ids and not isinstance(specific_file_ids, set):
794
specific_file_ids = set(specific_file_ids)
795
# TODO? Perhaps this should return the from_dir so that the root is
796
# yielded? or maybe an option?
798
if self.root is None:
800
# Optimize a common case
801
if (not yield_parents and specific_file_ids is not None and
802
len(specific_file_ids) == 1):
803
file_id = list(specific_file_ids)[0]
805
yield self.id2path(file_id), self[file_id]
808
if (specific_file_ids is None or yield_parents or
809
self.root.file_id in specific_file_ids):
811
elif isinstance(from_dir, basestring):
812
from_dir = self[from_dir]
814
if specific_file_ids is not None:
815
# TODO: jam 20070302 This could really be done as a loop rather
816
# than a bunch of recursive calls.
819
def add_ancestors(file_id):
820
if file_id not in byid:
822
parent_id = byid[file_id].parent_id
823
if parent_id is None:
825
if parent_id not in parents:
826
parents.add(parent_id)
827
add_ancestors(parent_id)
828
for file_id in specific_file_ids:
829
add_ancestors(file_id)
833
stack = [(u'', from_dir)]
835
cur_relpath, cur_dir = stack.pop()
838
for child_name, child_ie in sorted(cur_dir.children.iteritems()):
840
child_relpath = cur_relpath + child_name
842
if (specific_file_ids is None or
843
child_ie.file_id in specific_file_ids or
844
(yield_parents and child_ie.file_id in parents)):
845
yield child_relpath, child_ie
847
if child_ie.kind == 'directory':
848
if parents is None or child_ie.file_id in parents:
849
child_dirs.append((child_relpath+'/', child_ie))
850
stack.extend(reversed(child_dirs))
852
def _make_delta(self, old):
853
"""Make an inventory delta from two inventories."""
856
adds = new_ids - old_ids
857
deletes = old_ids - new_ids
858
common = old_ids.intersection(new_ids)
860
for file_id in deletes:
861
delta.append((old.id2path(file_id), None, file_id, None))
863
delta.append((None, self.id2path(file_id), file_id, self[file_id]))
864
for file_id in common:
865
if old[file_id] != self[file_id]:
866
delta.append((old.id2path(file_id), self.id2path(file_id),
867
file_id, self[file_id]))
870
def _get_mutable_inventory(self):
871
"""Returns a mutable copy of the object.
873
Some inventories are immutable, yet working trees, for example, needs
874
to mutate exisiting inventories instead of creating a new one.
876
raise NotImplementedError(self._get_mutable_inventory)
878
def make_entry(self, kind, name, parent_id, file_id=None):
879
"""Simple thunk to bzrlib.inventory.make_entry."""
880
return make_entry(kind, name, parent_id, file_id)
883
"""Return list of (path, ie) for all entries except the root.
885
This may be faster than iter_entries.
888
def descend(dir_ie, dir_path):
889
kids = dir_ie.children.items()
891
for name, ie in kids:
892
child_path = osutils.pathjoin(dir_path, name)
893
accum.append((child_path, ie))
894
if ie.kind == 'directory':
895
descend(ie, child_path)
897
descend(self.root, u'')
900
def directories(self):
901
"""Return (path, entry) pairs for all directories, including the root.
904
def descend(parent_ie, parent_path):
905
accum.append((parent_path, parent_ie))
907
kids = [(ie.name, ie) for ie in parent_ie.children.itervalues() if ie.kind == 'directory']
910
for name, child_ie in kids:
911
child_path = osutils.pathjoin(parent_path, name)
912
descend(child_ie, child_path)
913
descend(self.root, u'')
916
def path2id(self, relpath):
917
"""Walk down through directories to return entry of last component.
919
:param relpath: may be either a list of path components, or a single
920
string, in which case it is automatically split.
922
This returns the entry of the last component in the path,
923
which may be either a file or a directory.
925
Returns None IFF the path is not found.
927
if isinstance(relpath, basestring):
928
names = osutils.splitpath(relpath)
934
except errors.NoSuchId:
935
# root doesn't exist yet so nothing else can
941
children = getattr(parent, 'children', None)
950
return parent.file_id
952
def filter(self, specific_fileids):
953
"""Get an inventory view filtered against a set of file-ids.
955
Children of directories and parents are included.
957
The result may or may not reference the underlying inventory
958
so it should be treated as immutable.
960
interesting_parents = set()
961
for fileid in specific_fileids:
963
interesting_parents.update(self.get_idpath(fileid))
964
except errors.NoSuchId:
965
# This fileid is not in the inventory - that's ok
967
entries = self.iter_entries()
968
if self.root is None:
969
return Inventory(root_id=None)
970
other = Inventory(entries.next()[1].file_id)
971
other.root.revision = self.root.revision
972
other.revision_id = self.revision_id
973
directories_to_expand = set()
974
for path, entry in entries:
975
file_id = entry.file_id
976
if (file_id in specific_fileids
977
or entry.parent_id in directories_to_expand):
978
if entry.kind == 'directory':
979
directories_to_expand.add(file_id)
980
elif file_id not in interesting_parents:
982
other.add(entry.copy())
985
def get_idpath(self, file_id):
986
"""Return a list of file_ids for the path to an entry.
988
The list contains one element for each directory followed by
989
the id of the file itself. So the length of the returned list
990
is equal to the depth of the file in the tree, counting the
991
root directory as depth 1.
994
for parent in self._iter_file_id_parents(file_id):
995
p.insert(0, parent.file_id)
999
class Inventory(CommonInventory):
1000
"""Mutable dict based in-memory inventory.
1002
We never store the full path to a file, because renaming a directory
1003
implicitly moves all of its contents. This class internally maintains a
172
## mutter("read inventoryentry: %r" % (elt.attrib))
174
v = elt.get('text_size')
175
self.text_size = v and int(v)
180
from_element = classmethod(from_element)
182
def __cmp__(self, other):
185
if not isinstance(other, InventoryEntry):
186
return NotImplemented
188
return cmp(self.file_id, other.file_id) \
189
or cmp(self.name, other.name) \
190
or cmp(self.text_sha1, other.text_sha1) \
191
or cmp(self.text_size, other.text_size) \
192
or cmp(self.text_id, other.text_id) \
193
or cmp(self.parent_id, other.parent_id) \
194
or cmp(self.kind, other.kind)
198
class Inventory(XMLMixin):
199
"""Inventory of versioned files in a tree.
201
An Inventory acts like a set of InventoryEntry items. You can
202
also look files up by their file_id or name.
204
May be read from and written to a metadata file in a tree. To
205
manipulate the inventory (for example to add a file), it is read
206
in, modified, and then written back out.
208
The inventory represents a typical unix file tree, with
209
directories containing files and subdirectories. We never store
210
the full path to a file, because renaming a directory implicitly
211
moves all of its contents. This class internally maintains a
1004
212
lookup tree that allows the children under a directory to be
1005
213
returned quickly.
215
InventoryEntry objects must not be modified after they are
1007
218
>>> inv = Inventory()
1008
>>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
1009
InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
219
>>> inv.write_xml(sys.stdout)
222
>>> inv.add(InventoryEntry('123-123', 'hello.c'))
1010
223
>>> inv['123-123'].name
1013
Id's may be looked up from paths:
1015
>>> inv.path2id('hello.c')
225
>>> for file_id in inv: print file_id
229
May be treated as an iterator or set to look up file ids:
231
>>> bool(inv.path2id('hello.c'))
1017
233
>>> '123-123' in inv
1020
There are iterators over the contents:
1022
>>> [entry[0] for entry in inv.iter_entries()]
236
May also look up by name:
238
>>> [x[0] for x in inv.iter_entries()]
241
>>> inv.write_xml(sys.stdout)
243
<entry file_id="123-123" kind="file" name="hello.c" />
1026
def __init__(self, root_id=ROOT_ID, revision_id=None):
248
## TODO: Clear up handling of files in subdirectories; we probably
249
## do want to be able to just look them up by name but this
250
## probably means gradually walking down the path, looking up as we go.
252
## TODO: Make sure only canonical filenames are stored.
254
## TODO: Do something sensible about the possible collisions on
255
## case-losing filesystems. Perhaps we should just always forbid
258
## _tree should probably just be stored as
259
## InventoryEntry._children on each directory.
1027
262
"""Create or read an inventory.
1029
264
If a working directory is specified, the inventory is read
1030
265
from there. If the file is specified, read from that. If not,
1031
266
the inventory is created empty.
1033
The inventory is created with a default root directory, with
1036
if root_id is not None:
1037
self._set_root(InventoryDirectory(root_id, u'', None))
1041
self.revision_id = revision_id
1044
# More than one page of ouput is not useful anymore to debug
1047
contents = repr(self._byid)
1048
if len(contents) > max_len:
1049
contents = contents[:(max_len-len(closing))] + closing
1050
return "<Inventory object at %x, contents=%r>" % (id(self), contents)
1052
def apply_delta(self, delta):
1053
"""Apply a delta to this inventory.
1055
See the inventory developers documentation for the theory behind
1058
If delta application fails the inventory is left in an indeterminate
1059
state and must not be used.
1061
:param delta: A list of changes to apply. After all the changes are
1062
applied the final inventory must be internally consistent, but it
1063
is ok to supply changes which, if only half-applied would have an
1064
invalid result - such as supplying two changes which rename two
1065
files, 'A' and 'B' with each other : [('A', 'B', 'A-id', a_entry),
1066
('B', 'A', 'B-id', b_entry)].
1068
Each change is a tuple, of the form (old_path, new_path, file_id,
1071
When new_path is None, the change indicates the removal of an entry
1072
from the inventory and new_entry will be ignored (using None is
1073
appropriate). If new_path is not None, then new_entry must be an
1074
InventoryEntry instance, which will be incorporated into the
1075
inventory (and replace any existing entry with the same file id).
1077
When old_path is None, the change indicates the addition of
1078
a new entry to the inventory.
1080
When neither new_path nor old_path are None, the change is a
1081
modification to an entry, such as a rename, reparent, kind change
1084
The children attribute of new_entry is ignored. This is because
1085
this method preserves children automatically across alterations to
1086
the parent of the children, and cases where the parent id of a
1087
child is changing require the child to be passed in as a separate
1088
change regardless. E.g. in the recursive deletion of a directory -
1089
the directory's children must be included in the delta, or the
1090
final inventory will be invalid.
1092
Note that a file_id must only appear once within a given delta.
1093
An AssertionError is raised otherwise.
1095
# Check that the delta is legal. It would be nice if this could be
1096
# done within the loops below but it's safer to validate the delta
1097
# before starting to mutate the inventory, as there isn't a rollback
1099
list(_check_delta_unique_ids(_check_delta_unique_new_paths(
1100
_check_delta_unique_old_paths(_check_delta_ids_match_entry(
1101
_check_delta_ids_are_valid(
1102
_check_delta_new_path_entry_both_or_None(
1106
# Remove all affected items which were in the original inventory,
1107
# starting with the longest paths, thus ensuring parents are examined
1108
# after their children, which means that everything we examine has no
1109
# modified children remaining by the time we examine it.
1110
for old_path, file_id in sorted(((op, f) for op, np, f, e in delta
1111
if op is not None), reverse=True):
1112
# Preserve unaltered children of file_id for later reinsertion.
1113
file_id_children = getattr(self[file_id], 'children', {})
1114
if len(file_id_children):
1115
children[file_id] = file_id_children
1116
if self.id2path(file_id) != old_path:
1117
raise errors.InconsistentDelta(old_path, file_id,
1118
"Entry was at wrong other path %r." % self.id2path(file_id))
1119
# Remove file_id and the unaltered children. If file_id is not
1120
# being deleted it will be reinserted back later.
1121
self.remove_recursive_id(file_id)
1122
# Insert all affected which should be in the new inventory, reattaching
1123
# their children if they had any. This is done from shortest path to
1124
# longest, ensuring that items which were modified and whose parents in
1125
# the resulting inventory were also modified, are inserted after their
1127
for new_path, f, new_entry in sorted((np, f, e) for op, np, f, e in
1128
delta if np is not None):
1129
if new_entry.kind == 'directory':
1130
# Pop the child which to allow detection of children whose
1131
# parents were deleted and which were not reattached to a new
1133
replacement = InventoryDirectory(new_entry.file_id,
1134
new_entry.name, new_entry.parent_id)
1135
replacement.revision = new_entry.revision
1136
replacement.children = children.pop(replacement.file_id, {})
1137
new_entry = replacement
1140
except errors.DuplicateFileId:
1141
raise errors.InconsistentDelta(new_path, new_entry.file_id,
1142
"New id is already present in target.")
1143
except AttributeError:
1144
raise errors.InconsistentDelta(new_path, new_entry.file_id,
1145
"Parent is not a directory.")
1146
if self.id2path(new_entry.file_id) != new_path:
1147
raise errors.InconsistentDelta(new_path, new_entry.file_id,
1148
"New path is not consistent with parent path.")
1150
# Get the parent id that was deleted
1151
parent_id, children = children.popitem()
1152
raise errors.InconsistentDelta("<deleted>", parent_id,
1153
"The file id was deleted but its children were not deleted.")
1155
def create_by_apply_delta(self, inventory_delta, new_revision_id,
1156
propagate_caches=False):
1157
"""See CHKInventory.create_by_apply_delta()"""
1158
new_inv = self.copy()
1159
new_inv.apply_delta(inventory_delta)
1160
new_inv.revision_id = new_revision_id
1163
def _set_root(self, ie):
1165
self._byid = {self.root.file_id: self.root}
1168
# TODO: jam 20051218 Should copy also copy the revision_id?
1169
entries = self.iter_entries()
1170
if self.root is None:
1171
return Inventory(root_id=None)
1172
other = Inventory(entries.next()[1].file_id)
1173
other.root.revision = self.root.revision
1174
# copy recursively so we know directories will be added before
1175
# their children. There are more efficient ways than this...
1176
for path, entry in entries:
1177
other.add(entry.copy())
1180
def _get_mutable_inventory(self):
1181
"""See CommonInventory._get_mutable_inventory."""
1182
return copy.deepcopy(self)
270
# _tree is indexed by parent_id; at each level a map from name
271
# to ie. The None entry is the root.
272
self._tree = {None: {}}
1184
275
def __iter__(self):
1185
"""Iterate over all file-ids."""
1186
276
return iter(self._byid)
1188
def iter_just_entries(self):
1189
"""Iterate over all entries.
1191
Unlike iter_entries(), just the entries are returned (not (path, ie))
1192
and the order of entries is undefined.
1194
XXX: We may not want to merge this into bzr.dev.
1196
if self.root is None:
1198
for _, ie in self._byid.iteritems():
1201
279
def __len__(self):
1202
280
"""Returns number of entries."""
1203
281
return len(self._byid)
284
def iter_entries(self, parent_id=None):
285
"""Return (path, entry) pairs, in order by name."""
286
kids = self._tree[parent_id].items()
288
for name, ie in kids:
290
if ie.kind == 'directory':
291
for cn, cie in self.iter_entries(parent_id=ie.file_id):
292
yield joinpath([name, cn]), cie
295
def directories(self, include_root=True):
296
"""Return (path, entry) pairs for all directories.
300
for path, entry in self.iter_entries():
301
if entry.kind == 'directory':
306
def children(self, parent_id):
307
"""Return entries that are direct children of parent_id."""
308
return self._tree[parent_id]
312
# TODO: return all paths and entries
315
def __contains__(self, file_id):
316
"""True if this entry contains a file with given id.
318
>>> inv = Inventory()
319
>>> inv.add(InventoryEntry('123', 'foo.c'))
325
return file_id in self._byid
1205
328
def __getitem__(self, file_id):
1206
329
"""Return the entry for given file_id.
1208
331
>>> inv = Inventory()
1209
>>> inv.add(InventoryFile('123123', 'hello.c', ROOT_ID))
1210
InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
332
>>> inv.add(InventoryEntry('123123', 'hello.c'))
1211
333
>>> inv['123123'].name
1215
return self._byid[file_id]
1217
# really we're passing an inventory, not a tree...
1218
raise errors.NoSuchId(self, file_id)
1220
def get_file_kind(self, file_id):
1221
return self._byid[file_id].kind
1223
def get_child(self, parent_id, filename):
1224
return self[parent_id].children.get(filename)
1226
def _add_child(self, entry):
1227
"""Add an entry to the inventory, without adding it to its parent"""
1228
if entry.file_id in self._byid:
1229
raise BzrError("inventory already contains entry with id {%s}" %
1231
self._byid[entry.file_id] = entry
1232
for child in getattr(entry, 'children', {}).itervalues():
1233
self._add_child(child)
336
return self._byid[file_id]
1236
339
def add(self, entry):
1237
340
"""Add entry to inventory.
1239
342
To add a file to a branch ready to be committed, use Branch.add,
1244
if entry.file_id in self._byid:
1245
raise errors.DuplicateFileId(entry.file_id,
1246
self._byid[entry.file_id])
1247
if entry.parent_id is None:
1251
parent = self._byid[entry.parent_id]
1253
raise errors.InconsistentDelta("<unknown>", entry.parent_id,
1254
"Parent not in inventory.")
1255
if entry.name in parent.children:
1256
raise errors.InconsistentDelta(
1257
self.id2path(parent.children[entry.name].file_id),
1259
"Path already versioned")
1260
parent.children[entry.name] = entry
1261
return self._add_child(entry)
1263
def add_path(self, relpath, kind, file_id=None, parent_id=None):
1264
"""Add entry from a path.
1266
The immediate parent must already be versioned.
1268
Returns the new entry object."""
1270
parts = osutils.splitpath(relpath)
1274
file_id = generate_ids.gen_root_id()
1275
self.root = InventoryDirectory(file_id, '', None)
1276
self._byid = {self.root.file_id: self.root}
1279
parent_path = parts[:-1]
1280
parent_id = self.path2id(parent_path)
1281
if parent_id is None:
1282
raise errors.NotVersionedError(path=parent_path)
1283
ie = make_entry(kind, parts[-1], parent_id, file_id)
344
if entry.file_id in self:
345
bailout("inventory already contains entry with id {%s}" % entry.file_id)
347
if entry.parent_id != None:
348
if entry.parent_id not in self:
349
bailout("parent_id %s of new entry not found in inventory"
352
if self._tree[entry.parent_id].has_key(entry.name):
353
bailout("%s is already versioned"
354
% appendpath(self.id2path(entry.parent_id), entry.name))
356
self._byid[entry.file_id] = entry
357
self._tree[entry.parent_id][entry.name] = entry
359
if entry.kind == 'directory':
360
self._tree[entry.file_id] = {}
1286
363
def __delitem__(self, file_id):
1287
364
"""Remove entry by id.
1289
366
>>> inv = Inventory()
1290
>>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
1291
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
367
>>> inv.add(InventoryEntry('123', 'foo.c'))
1292
368
>>> '123' in inv
1294
370
>>> del inv['123']
1298
374
ie = self[file_id]
376
assert self._tree[ie.parent_id][ie.name] == ie
378
# TODO: Test deleting all children; maybe hoist to a separate
380
if ie.kind == 'directory':
381
for cie in self._tree[file_id].values():
382
del self[cie.file_id]
383
del self._tree[file_id]
1299
385
del self._byid[file_id]
1300
if ie.parent_id is not None:
1301
del self[ie.parent_id].children[ie.name]
1303
def __eq__(self, other):
386
del self._tree[ie.parent_id][ie.name]
390
return Set(self._byid)
393
def to_element(self):
394
"""Convert to XML Element"""
395
e = Element('inventory')
397
for path, ie in self.iter_entries():
398
e.append(ie.to_element())
402
def from_element(cls, elt):
403
"""Construct from XML Element
405
>>> inv = Inventory()
406
>>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c'))
407
>>> elt = inv.to_element()
408
>>> inv2 = Inventory.from_element(elt)
412
assert elt.tag == 'inventory'
415
o.add(InventoryEntry.from_element(e))
418
from_element = classmethod(from_element)
421
def __cmp__(self, other):
1304
422
"""Compare two sets by comparing their contents.
1306
424
>>> i1 = Inventory()
1307
425
>>> i2 = Inventory()
1310
>>> i1.add(InventoryFile('123', 'foo', ROOT_ID))
1311
InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
428
>>> i1.add(InventoryEntry('123', 'foo'))
1314
>>> i2.add(InventoryFile('123', 'foo', ROOT_ID))
1315
InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
431
>>> i2.add(InventoryEntry('123', 'foo'))
1319
438
if not isinstance(other, Inventory):
1320
439
return NotImplemented
1322
return self._byid == other._byid
1324
def __ne__(self, other):
1325
return not self.__eq__(other)
1328
raise ValueError('not hashable')
1330
def _iter_file_id_parents(self, file_id):
1331
"""Yield the parents of file_id up to the root."""
1332
while file_id is not None:
1334
ie = self._byid[file_id]
1336
raise errors.NoSuchId(tree=None, file_id=file_id)
1338
file_id = ie.parent_id
1340
def has_id(self, file_id):
1341
return (file_id in self._byid)
1343
def _make_delta(self, old):
1344
"""Make an inventory delta from two inventories."""
1345
old_getter = getattr(old, '_byid', old)
1346
new_getter = self._byid
1347
old_ids = set(old_getter)
1348
new_ids = set(new_getter)
1349
adds = new_ids - old_ids
1350
deletes = old_ids - new_ids
1351
if not adds and not deletes:
1354
common = old_ids.intersection(new_ids)
1356
for file_id in deletes:
1357
delta.append((old.id2path(file_id), None, file_id, None))
1358
for file_id in adds:
1359
delta.append((None, self.id2path(file_id), file_id, self[file_id]))
1360
for file_id in common:
1361
new_ie = new_getter[file_id]
1362
old_ie = old_getter[file_id]
1363
# If xml_serializer returns the cached InventoryEntries (rather
1364
# than always doing .copy()), inlining the 'is' check saves 2.7M
1365
# calls to __eq__. Under lsprof this saves 20s => 6s.
1366
# It is a minor improvement without lsprof.
1367
if old_ie is new_ie or old_ie == new_ie:
1370
delta.append((old.id2path(file_id), self.id2path(file_id),
1374
def remove_recursive_id(self, file_id):
1375
"""Remove file_id, and children, from the inventory.
1377
:param file_id: A file_id to remove.
1379
to_find_delete = [self._byid[file_id]]
1381
while to_find_delete:
1382
ie = to_find_delete.pop()
1383
to_delete.append(ie.file_id)
1384
if ie.kind == 'directory':
1385
to_find_delete.extend(ie.children.values())
1386
for file_id in reversed(to_delete):
441
if self.id_set() ^ other.id_set():
444
for file_id in self._byid:
445
c = cmp(self[file_id], other[file_id])
451
def id2path(self, file_id):
452
"""Return as a list the path to file_id."""
454
while file_id != None:
1387
455
ie = self[file_id]
1388
del self._byid[file_id]
1389
if ie.parent_id is not None:
1390
del self[ie.parent_id].children[ie.name]
1394
def rename(self, file_id, new_parent_id, new_name):
1395
"""Move a file within the inventory.
1397
This can change either the name, or the parent, or both.
1399
This does not move the working file.
1401
new_name = ensure_normalized_name(new_name)
1402
if not is_valid_name(new_name):
1403
raise BzrError("not an acceptable filename: %r" % new_name)
1405
new_parent = self._byid[new_parent_id]
1406
if new_name in new_parent.children:
1407
raise BzrError("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
1409
new_parent_idpath = self.get_idpath(new_parent_id)
1410
if file_id in new_parent_idpath:
1411
raise BzrError("cannot move directory %r into a subdirectory of itself, %r"
1412
% (self.id2path(file_id), self.id2path(new_parent_id)))
1414
file_ie = self._byid[file_id]
1415
old_parent = self._byid[file_ie.parent_id]
1417
# TODO: Don't leave things messed up if this fails
1419
del old_parent.children[file_ie.name]
1420
new_parent.children[new_name] = file_ie
1422
file_ie.name = new_name
1423
file_ie.parent_id = new_parent_id
1425
def is_root(self, file_id):
1426
return self.root is not None and file_id == self.root.file_id
1429
class CHKInventory(CommonInventory):
1430
"""An inventory persisted in a CHK store.
1432
By design, a CHKInventory is immutable so many of the methods
1433
supported by Inventory - add, rename, apply_delta, etc - are *not*
1434
supported. To create a new CHKInventory, use create_by_apply_delta()
1435
or from_inventory(), say.
1437
Internally, a CHKInventory has one or two CHKMaps:
1439
* id_to_entry - a map from (file_id,) => InventoryEntry as bytes
1440
* parent_id_basename_to_file_id - a map from (parent_id, basename_utf8)
1443
The second map is optional and not present in early CHkRepository's.
1445
No caching is performed: every method call or item access will perform
1446
requests to the storage layer. As such, keep references to objects you
1450
def __init__(self, search_key_name):
1451
CommonInventory.__init__(self)
1452
self._fileid_to_entry_cache = {}
1453
self._path_to_fileid_cache = {}
1454
self._search_key_name = search_key_name
1457
def __eq__(self, other):
1458
"""Compare two sets by comparing their contents."""
1459
if not isinstance(other, CHKInventory):
1460
return NotImplemented
1462
this_key = self.id_to_entry.key()
1463
other_key = other.id_to_entry.key()
1464
this_pid_key = self.parent_id_basename_to_file_id.key()
1465
other_pid_key = other.parent_id_basename_to_file_id.key()
1466
if None in (this_key, this_pid_key, other_key, other_pid_key):
1468
return this_key == other_key and this_pid_key == other_pid_key
1470
def _entry_to_bytes(self, entry):
1471
"""Serialise entry as a single bytestring.
1473
:param Entry: An inventory entry.
1474
:return: A bytestring for the entry.
1477
ENTRY ::= FILE | DIR | SYMLINK | TREE
1478
FILE ::= "file: " COMMON SEP SHA SEP SIZE SEP EXECUTABLE
1479
DIR ::= "dir: " COMMON
1480
SYMLINK ::= "symlink: " COMMON SEP TARGET_UTF8
1481
TREE ::= "tree: " COMMON REFERENCE_REVISION
1482
COMMON ::= FILE_ID SEP PARENT_ID SEP NAME_UTF8 SEP REVISION
1485
if entry.parent_id is not None:
1486
parent_str = entry.parent_id
1489
name_str = entry.name.encode("utf8")
1490
if entry.kind == 'file':
1491
if entry.executable:
1495
return "file: %s\n%s\n%s\n%s\n%s\n%d\n%s" % (
1496
entry.file_id, parent_str, name_str, entry.revision,
1497
entry.text_sha1, entry.text_size, exec_str)
1498
elif entry.kind == 'directory':
1499
return "dir: %s\n%s\n%s\n%s" % (
1500
entry.file_id, parent_str, name_str, entry.revision)
1501
elif entry.kind == 'symlink':
1502
return "symlink: %s\n%s\n%s\n%s\n%s" % (
1503
entry.file_id, parent_str, name_str, entry.revision,
1504
entry.symlink_target.encode("utf8"))
1505
elif entry.kind == 'tree-reference':
1506
return "tree: %s\n%s\n%s\n%s\n%s" % (
1507
entry.file_id, parent_str, name_str, entry.revision,
1508
entry.reference_revision)
1510
raise ValueError("unknown kind %r" % entry.kind)
1512
def _expand_fileids_to_parents_and_children(self, file_ids):
1513
"""Give a more wholistic view starting with the given file_ids.
1515
For any file_id which maps to a directory, we will include all children
1516
of that directory. We will also include all directories which are
1517
parents of the given file_ids, but we will not include their children.
1524
fringle # fringle-id
1528
if given [foo-id] we will include
1529
TREE_ROOT as interesting parents
1531
foo-id, baz-id, frob-id, fringle-id
1535
# TODO: Pre-pass over the list of fileids to see if anything is already
1536
# deserialized in self._fileid_to_entry_cache
1538
directories_to_expand = set()
1539
children_of_parent_id = {}
1540
# It is okay if some of the fileids are missing
1541
for entry in self._getitems(file_ids):
1542
if entry.kind == 'directory':
1543
directories_to_expand.add(entry.file_id)
1544
interesting.add(entry.parent_id)
1545
children_of_parent_id.setdefault(entry.parent_id, []
1546
).append(entry.file_id)
1548
# Now, interesting has all of the direct parents, but not the
1549
# parents of those parents. It also may have some duplicates with
1551
remaining_parents = interesting.difference(file_ids)
1552
# When we hit the TREE_ROOT, we'll get an interesting parent of None,
1553
# but we don't actually want to recurse into that
1554
interesting.add(None) # this will auto-filter it in the loop
1555
remaining_parents.discard(None)
1556
while remaining_parents:
1557
next_parents = set()
1558
for entry in self._getitems(remaining_parents):
1559
next_parents.add(entry.parent_id)
1560
children_of_parent_id.setdefault(entry.parent_id, []
1561
).append(entry.file_id)
1562
# Remove any search tips we've already processed
1563
remaining_parents = next_parents.difference(interesting)
1564
interesting.update(remaining_parents)
1565
# We should probably also .difference(directories_to_expand)
1566
interesting.update(file_ids)
1567
interesting.discard(None)
1568
while directories_to_expand:
1569
# Expand directories by looking in the
1570
# parent_id_basename_to_file_id map
1571
keys = [StaticTuple(f,).intern() for f in directories_to_expand]
1572
directories_to_expand = set()
1573
items = self.parent_id_basename_to_file_id.iteritems(keys)
1574
next_file_ids = set([item[1] for item in items])
1575
next_file_ids = next_file_ids.difference(interesting)
1576
interesting.update(next_file_ids)
1577
for entry in self._getitems(next_file_ids):
1578
if entry.kind == 'directory':
1579
directories_to_expand.add(entry.file_id)
1580
children_of_parent_id.setdefault(entry.parent_id, []
1581
).append(entry.file_id)
1582
return interesting, children_of_parent_id
1584
def filter(self, specific_fileids):
1585
"""Get an inventory view filtered against a set of file-ids.
1587
Children of directories and parents are included.
1589
The result may or may not reference the underlying inventory
1590
so it should be treated as immutable.
1593
parent_to_children) = self._expand_fileids_to_parents_and_children(
1595
# There is some overlap here, but we assume that all interesting items
1596
# are in the _fileid_to_entry_cache because we had to read them to
1597
# determine if they were a dir we wanted to recurse, or just a file
1598
# This should give us all the entries we'll want to add, so start
1600
other = Inventory(self.root_id)
1601
other.root.revision = self.root.revision
1602
other.revision_id = self.revision_id
1603
if not interesting or not parent_to_children:
1604
# empty filter, or filtering entrys that don't exist
1605
# (if even 1 existed, then we would have populated
1606
# parent_to_children with at least the tree root.)
1608
cache = self._fileid_to_entry_cache
1609
remaining_children = collections.deque(parent_to_children[self.root_id])
1610
while remaining_children:
1611
file_id = remaining_children.popleft()
1613
if ie.kind == 'directory':
1614
ie = ie.copy() # We create a copy to depopulate the .children attribute
1615
# TODO: depending on the uses of 'other' we should probably alwyas
1616
# '.copy()' to prevent someone from mutating other and
1617
# invaliding our internal cache
1619
if file_id in parent_to_children:
1620
remaining_children.extend(parent_to_children[file_id])
1624
def _bytes_to_utf8name_key(bytes):
1625
"""Get the file_id, revision_id key out of bytes."""
1626
# We don't normally care about name, except for times when we want
1627
# to filter out empty names because of non rich-root...
1628
sections = bytes.split('\n')
1629
kind, file_id = sections[0].split(': ')
1630
return (sections[2], intern(file_id), intern(sections[3]))
1632
def _bytes_to_entry(self, bytes):
1633
"""Deserialise a serialised entry."""
1634
sections = bytes.split('\n')
1635
if sections[0].startswith("file: "):
1636
result = InventoryFile(sections[0][6:],
1637
sections[2].decode('utf8'),
1639
result.text_sha1 = sections[4]
1640
result.text_size = int(sections[5])
1641
result.executable = sections[6] == "Y"
1642
elif sections[0].startswith("dir: "):
1643
result = CHKInventoryDirectory(sections[0][5:],
1644
sections[2].decode('utf8'),
1646
elif sections[0].startswith("symlink: "):
1647
result = InventoryLink(sections[0][9:],
1648
sections[2].decode('utf8'),
1650
result.symlink_target = sections[4].decode('utf8')
1651
elif sections[0].startswith("tree: "):
1652
result = TreeReference(sections[0][6:],
1653
sections[2].decode('utf8'),
1655
result.reference_revision = sections[4]
1657
raise ValueError("Not a serialised entry %r" % bytes)
1658
result.file_id = intern(result.file_id)
1659
result.revision = intern(sections[3])
1660
if result.parent_id == '':
1661
result.parent_id = None
1662
self._fileid_to_entry_cache[result.file_id] = result
1665
def _get_mutable_inventory(self):
1666
"""See CommonInventory._get_mutable_inventory."""
1667
entries = self.iter_entries()
1668
inv = Inventory(None, self.revision_id)
1669
for path, inv_entry in entries:
1670
inv.add(inv_entry.copy())
1673
def create_by_apply_delta(self, inventory_delta, new_revision_id,
1674
propagate_caches=False):
1675
"""Create a new CHKInventory by applying inventory_delta to this one.
1677
See the inventory developers documentation for the theory behind
1680
:param inventory_delta: The inventory delta to apply. See
1681
Inventory.apply_delta for details.
1682
:param new_revision_id: The revision id of the resulting CHKInventory.
1683
:param propagate_caches: If True, the caches for this inventory are
1684
copied to and updated for the result.
1685
:return: The new CHKInventory.
1687
split = osutils.split
1688
result = CHKInventory(self._search_key_name)
1689
if propagate_caches:
1690
# Just propagate the path-to-fileid cache for now
1691
result._path_to_fileid_cache = dict(self._path_to_fileid_cache.iteritems())
1692
search_key_func = chk_map.search_key_registry.get(self._search_key_name)
1693
self.id_to_entry._ensure_root()
1694
maximum_size = self.id_to_entry._root_node.maximum_size
1695
result.revision_id = new_revision_id
1696
result.id_to_entry = chk_map.CHKMap(
1697
self.id_to_entry._store,
1698
self.id_to_entry.key(),
1699
search_key_func=search_key_func)
1700
result.id_to_entry._ensure_root()
1701
result.id_to_entry._root_node.set_maximum_size(maximum_size)
1702
# Change to apply to the parent_id_basename delta. The dict maps
1703
# (parent_id, basename) -> (old_key, new_value). We use a dict because
1704
# when a path has its id replaced (e.g. the root is changed, or someone
1705
# does bzr mv a b, bzr mv c a, we should output a single change to this
1706
# map rather than two.
1707
parent_id_basename_delta = {}
1708
if self.parent_id_basename_to_file_id is not None:
1709
result.parent_id_basename_to_file_id = chk_map.CHKMap(
1710
self.parent_id_basename_to_file_id._store,
1711
self.parent_id_basename_to_file_id.key(),
1712
search_key_func=search_key_func)
1713
result.parent_id_basename_to_file_id._ensure_root()
1714
self.parent_id_basename_to_file_id._ensure_root()
1715
result_p_id_root = result.parent_id_basename_to_file_id._root_node
1716
p_id_root = self.parent_id_basename_to_file_id._root_node
1717
result_p_id_root.set_maximum_size(p_id_root.maximum_size)
1718
result_p_id_root._key_width = p_id_root._key_width
1720
result.parent_id_basename_to_file_id = None
1721
result.root_id = self.root_id
1722
id_to_entry_delta = []
1723
# inventory_delta is only traversed once, so we just update the
1725
# Check for repeated file ids
1726
inventory_delta = _check_delta_unique_ids(inventory_delta)
1727
# Repeated old paths
1728
inventory_delta = _check_delta_unique_old_paths(inventory_delta)
1729
# Check for repeated new paths
1730
inventory_delta = _check_delta_unique_new_paths(inventory_delta)
1731
# Check for entries that don't match the fileid
1732
inventory_delta = _check_delta_ids_match_entry(inventory_delta)
1733
# Check for nonsense fileids
1734
inventory_delta = _check_delta_ids_are_valid(inventory_delta)
1735
# Check for new_path <-> entry consistency
1736
inventory_delta = _check_delta_new_path_entry_both_or_None(
1738
# All changed entries need to have their parents be directories and be
1739
# at the right path. This set contains (path, id) tuples.
1741
# When we delete an item, all the children of it must be either deleted
1742
# or altered in their own right. As we batch process the change via
1743
# CHKMap.apply_delta, we build a set of things to use to validate the
1747
for old_path, new_path, file_id, entry in inventory_delta:
1750
result.root_id = file_id
1751
if new_path is None:
1756
if propagate_caches:
1758
del result._path_to_fileid_cache[old_path]
1761
deletes.add(file_id)
1763
new_key = StaticTuple(file_id,)
1764
new_value = result._entry_to_bytes(entry)
1765
# Update caches. It's worth doing this whether
1766
# we're propagating the old caches or not.
1767
result._path_to_fileid_cache[new_path] = file_id
1768
parents.add((split(new_path)[0], entry.parent_id))
1769
if old_path is None:
1772
old_key = StaticTuple(file_id,)
1773
if self.id2path(file_id) != old_path:
1774
raise errors.InconsistentDelta(old_path, file_id,
1775
"Entry was at wrong other path %r." %
1776
self.id2path(file_id))
1777
altered.add(file_id)
1778
id_to_entry_delta.append(StaticTuple(old_key, new_key, new_value))
1779
if result.parent_id_basename_to_file_id is not None:
1780
# parent_id, basename changes
1781
if old_path is None:
1784
old_entry = self[file_id]
1785
old_key = self._parent_id_basename_key(old_entry)
1786
if new_path is None:
1790
new_key = self._parent_id_basename_key(entry)
1792
# If the two keys are the same, the value will be unchanged
1793
# as its always the file id for this entry.
1794
if old_key != new_key:
1795
# Transform a change into explicit delete/add preserving
1796
# a possible match on the key from a different file id.
1797
if old_key is not None:
1798
parent_id_basename_delta.setdefault(
1799
old_key, [None, None])[0] = old_key
1800
if new_key is not None:
1801
parent_id_basename_delta.setdefault(
1802
new_key, [None, None])[1] = new_value
1803
# validate that deletes are complete.
1804
for file_id in deletes:
1805
entry = self[file_id]
1806
if entry.kind != 'directory':
1808
# This loop could potentially be better by using the id_basename
1809
# map to just get the child file ids.
1810
for child in entry.children.values():
1811
if child.file_id not in altered:
1812
raise errors.InconsistentDelta(self.id2path(child.file_id),
1813
child.file_id, "Child not deleted or reparented when "
1815
result.id_to_entry.apply_delta(id_to_entry_delta)
1816
if parent_id_basename_delta:
1817
# Transform the parent_id_basename delta data into a linear delta
1818
# with only one record for a given key. Optimally this would allow
1819
# re-keying, but its simpler to just output that as a delete+add
1820
# to spend less time calculating the delta.
1822
for key, (old_key, value) in parent_id_basename_delta.iteritems():
1823
if value is not None:
1824
delta_list.append((old_key, key, value))
1826
delta_list.append((old_key, None, None))
1827
result.parent_id_basename_to_file_id.apply_delta(delta_list)
1828
parents.discard(('', None))
1829
for parent_path, parent in parents:
457
file_id = ie.parent_id
462
def path2id(self, name):
463
"""Walk down through directories to return entry of last component.
465
names may be either a list of path components, or a single
466
string, in which case it is automatically split.
468
This returns the entry of the last component in the path,
469
which may be either a file or a directory.
471
assert isinstance(name, types.StringTypes)
474
for f in splitpath(name):
1831
if result[parent].kind != 'directory':
1832
raise errors.InconsistentDelta(result.id2path(parent), parent,
1833
'Not a directory, but given children')
1834
except errors.NoSuchId:
1835
raise errors.InconsistentDelta("<unknown>", parent,
1836
"Parent is not present in resulting inventory.")
1837
if result.path2id(parent_path) != parent:
1838
raise errors.InconsistentDelta(parent_path, parent,
1839
"Parent has wrong path %r." % result.path2id(parent_path))
1843
def deserialise(klass, chk_store, bytes, expected_revision_id):
1844
"""Deserialise a CHKInventory.
1846
:param chk_store: A CHK capable VersionedFiles instance.
1847
:param bytes: The serialised bytes.
1848
:param expected_revision_id: The revision ID we think this inventory is
1850
:return: A CHKInventory
1852
lines = bytes.split('\n')
1854
raise AssertionError('bytes to deserialize must end with an eol')
1856
if lines[0] != 'chkinventory:':
1857
raise ValueError("not a serialised CHKInventory: %r" % bytes)
1859
allowed_keys = frozenset(['root_id', 'revision_id', 'search_key_name',
1860
'parent_id_basename_to_file_id',
1862
for line in lines[1:]:
1863
key, value = line.split(': ', 1)
1864
if key not in allowed_keys:
1865
raise errors.BzrError('Unknown key in inventory: %r\n%r'
1868
raise errors.BzrError('Duplicate key in inventory: %r\n%r'
1871
revision_id = intern(info['revision_id'])
1872
root_id = intern(info['root_id'])
1873
search_key_name = intern(info.get('search_key_name', 'plain'))
1874
parent_id_basename_to_file_id = intern(info.get(
1875
'parent_id_basename_to_file_id', None))
1876
if not parent_id_basename_to_file_id.startswith('sha1:'):
1877
raise ValueError('parent_id_basename_to_file_id should be a sha1'
1878
' key not %r' % (parent_id_basename_to_file_id,))
1879
id_to_entry = info['id_to_entry']
1880
if not id_to_entry.startswith('sha1:'):
1881
raise ValueError('id_to_entry should be a sha1'
1882
' key not %r' % (id_to_entry,))
1884
result = CHKInventory(search_key_name)
1885
result.revision_id = revision_id
1886
result.root_id = root_id
1887
search_key_func = chk_map.search_key_registry.get(
1888
result._search_key_name)
1889
if parent_id_basename_to_file_id is not None:
1890
result.parent_id_basename_to_file_id = chk_map.CHKMap(
1891
chk_store, StaticTuple(parent_id_basename_to_file_id,),
1892
search_key_func=search_key_func)
1894
result.parent_id_basename_to_file_id = None
1896
result.id_to_entry = chk_map.CHKMap(chk_store,
1897
StaticTuple(id_to_entry,),
1898
search_key_func=search_key_func)
1899
if (result.revision_id,) != expected_revision_id:
1900
raise ValueError("Mismatched revision id and expected: %r, %r" %
1901
(result.revision_id, expected_revision_id))
1905
def from_inventory(klass, chk_store, inventory, maximum_size=0, search_key_name='plain'):
1906
"""Create a CHKInventory from an existing inventory.
1908
The content of inventory is copied into the chk_store, and a
1909
CHKInventory referencing that is returned.
1911
:param chk_store: A CHK capable VersionedFiles instance.
1912
:param inventory: The inventory to copy.
1913
:param maximum_size: The CHKMap node size limit.
1914
:param search_key_name: The identifier for the search key function
1916
result = klass(search_key_name)
1917
result.revision_id = inventory.revision_id
1918
result.root_id = inventory.root.file_id
1920
entry_to_bytes = result._entry_to_bytes
1921
parent_id_basename_key = result._parent_id_basename_key
1922
id_to_entry_dict = {}
1923
parent_id_basename_dict = {}
1924
for path, entry in inventory.iter_entries():
1925
key = StaticTuple(entry.file_id,).intern()
1926
id_to_entry_dict[key] = entry_to_bytes(entry)
1927
p_id_key = parent_id_basename_key(entry)
1928
parent_id_basename_dict[p_id_key] = entry.file_id
1930
result._populate_from_dicts(chk_store, id_to_entry_dict,
1931
parent_id_basename_dict, maximum_size=maximum_size)
1934
def _populate_from_dicts(self, chk_store, id_to_entry_dict,
1935
parent_id_basename_dict, maximum_size):
1936
search_key_func = chk_map.search_key_registry.get(self._search_key_name)
1937
root_key = chk_map.CHKMap.from_dict(chk_store, id_to_entry_dict,
1938
maximum_size=maximum_size, key_width=1,
1939
search_key_func=search_key_func)
1940
self.id_to_entry = chk_map.CHKMap(chk_store, root_key,
1942
root_key = chk_map.CHKMap.from_dict(chk_store,
1943
parent_id_basename_dict,
1944
maximum_size=maximum_size, key_width=2,
1945
search_key_func=search_key_func)
1946
self.parent_id_basename_to_file_id = chk_map.CHKMap(chk_store,
1947
root_key, search_key_func)
1949
def _parent_id_basename_key(self, entry):
1950
"""Create a key for a entry in a parent_id_basename_to_file_id index."""
1951
if entry.parent_id is not None:
1952
parent_id = entry.parent_id
1955
return StaticTuple(parent_id, entry.name.encode('utf8')).intern()
1957
def __getitem__(self, file_id):
1958
"""map a single file_id -> InventoryEntry."""
1960
raise errors.NoSuchId(self, file_id)
1961
result = self._fileid_to_entry_cache.get(file_id, None)
1962
if result is not None:
1965
return self._bytes_to_entry(
1966
self.id_to_entry.iteritems([StaticTuple(file_id,)]).next()[1])
1967
except StopIteration:
1968
# really we're passing an inventory, not a tree...
1969
raise errors.NoSuchId(self, file_id)
1971
def _getitems(self, file_ids):
1972
"""Similar to __getitem__, but lets you query for multiple.
1974
The returned order is undefined. And currently if an item doesn't
1975
exist, it isn't included in the output.
1979
for file_id in file_ids:
1980
entry = self._fileid_to_entry_cache.get(file_id, None)
1982
remaining.append(file_id)
1984
result.append(entry)
1985
file_keys = [StaticTuple(f,).intern() for f in remaining]
1986
for file_key, value in self.id_to_entry.iteritems(file_keys):
1987
entry = self._bytes_to_entry(value)
1988
result.append(entry)
1989
self._fileid_to_entry_cache[entry.file_id] = entry
476
cie = self._tree[parent_id][f]
478
parent_id = cie.file_id
486
def get_child(self, parent_id, child_name):
487
return self._tree[parent_id].get(child_name)
490
def has_filename(self, names):
491
return bool(self.path2id(names))
1992
494
def has_id(self, file_id):
1993
# Perhaps have an explicit 'contains' method on CHKMap ?
1994
if self._fileid_to_entry_cache.get(file_id, None) is not None:
1997
self.id_to_entry.iteritems([StaticTuple(file_id,)]))) == 1
1999
def is_root(self, file_id):
2000
return file_id == self.root_id
2002
def _iter_file_id_parents(self, file_id):
2003
"""Yield the parents of file_id up to the root."""
2004
while file_id is not None:
2008
raise errors.NoSuchId(tree=self, file_id=file_id)
2010
file_id = ie.parent_id
2013
"""Iterate over all file-ids."""
2014
for key, _ in self.id_to_entry.iteritems():
2017
def iter_just_entries(self):
2018
"""Iterate over all entries.
2020
Unlike iter_entries(), just the entries are returned (not (path, ie))
2021
and the order of entries is undefined.
2023
XXX: We may not want to merge this into bzr.dev.
2025
for key, entry in self.id_to_entry.iteritems():
2027
ie = self._fileid_to_entry_cache.get(file_id, None)
2029
ie = self._bytes_to_entry(entry)
2030
self._fileid_to_entry_cache[file_id] = ie
2033
def iter_changes(self, basis):
2034
"""Generate a Tree.iter_changes change list between this and basis.
2036
:param basis: Another CHKInventory.
2037
:return: An iterator over the changes between self and basis, as per
2038
tree.iter_changes().
2040
# We want: (file_id, (path_in_source, path_in_target),
2041
# changed_content, versioned, parent, name, kind,
2043
for key, basis_value, self_value in \
2044
self.id_to_entry.iter_changes(basis.id_to_entry):
2046
if basis_value is not None:
2047
basis_entry = basis._bytes_to_entry(basis_value)
2048
path_in_source = basis.id2path(file_id)
2049
basis_parent = basis_entry.parent_id
2050
basis_name = basis_entry.name
2051
basis_executable = basis_entry.executable
2053
path_in_source = None
2056
basis_executable = None
2057
if self_value is not None:
2058
self_entry = self._bytes_to_entry(self_value)
2059
path_in_target = self.id2path(file_id)
2060
self_parent = self_entry.parent_id
2061
self_name = self_entry.name
2062
self_executable = self_entry.executable
2064
path_in_target = None
2067
self_executable = None
2068
if basis_value is None:
2070
kind = (None, self_entry.kind)
2071
versioned = (False, True)
2072
elif self_value is None:
2074
kind = (basis_entry.kind, None)
2075
versioned = (True, False)
2077
kind = (basis_entry.kind, self_entry.kind)
2078
versioned = (True, True)
2079
changed_content = False
2080
if kind[0] != kind[1]:
2081
changed_content = True
2082
elif kind[0] == 'file':
2083
if (self_entry.text_size != basis_entry.text_size or
2084
self_entry.text_sha1 != basis_entry.text_sha1):
2085
changed_content = True
2086
elif kind[0] == 'symlink':
2087
if self_entry.symlink_target != basis_entry.symlink_target:
2088
changed_content = True
2089
elif kind[0] == 'tree-reference':
2090
if (self_entry.reference_revision !=
2091
basis_entry.reference_revision):
2092
changed_content = True
2093
parent = (basis_parent, self_parent)
2094
name = (basis_name, self_name)
2095
executable = (basis_executable, self_executable)
2096
if (not changed_content
2097
and parent[0] == parent[1]
2098
and name[0] == name[1]
2099
and executable[0] == executable[1]):
2100
# Could happen when only the revision changed for a directory
2103
yield (file_id, (path_in_source, path_in_target), changed_content,
2104
versioned, parent, name, kind, executable)
2107
"""Return the number of entries in the inventory."""
2108
return len(self.id_to_entry)
2110
def _make_delta(self, old):
2111
"""Make an inventory delta from two inventories."""
2112
if type(old) != CHKInventory:
2113
return CommonInventory._make_delta(self, old)
2115
for key, old_value, self_value in \
2116
self.id_to_entry.iter_changes(old.id_to_entry):
2118
if old_value is not None:
2119
old_path = old.id2path(file_id)
2122
if self_value is not None:
2123
entry = self._bytes_to_entry(self_value)
2124
self._fileid_to_entry_cache[file_id] = entry
2125
new_path = self.id2path(file_id)
2129
delta.append((old_path, new_path, file_id, entry))
2132
def path2id(self, relpath):
2133
"""See CommonInventory.path2id()."""
2134
# TODO: perhaps support negative hits?
2135
result = self._path_to_fileid_cache.get(relpath, None)
2136
if result is not None:
2138
if isinstance(relpath, basestring):
2139
names = osutils.splitpath(relpath)
2142
current_id = self.root_id
2143
if current_id is None:
2145
parent_id_index = self.parent_id_basename_to_file_id
2147
for basename in names:
2148
if cur_path is None:
2151
cur_path = cur_path + '/' + basename
2152
basename_utf8 = basename.encode('utf8')
2153
file_id = self._path_to_fileid_cache.get(cur_path, None)
2155
key_filter = [StaticTuple(current_id, basename_utf8)]
2156
items = parent_id_index.iteritems(key_filter)
2157
for (parent_id, name_utf8), file_id in items:
2158
if parent_id != current_id or name_utf8 != basename_utf8:
2159
raise errors.BzrError("corrupt inventory lookup! "
2160
"%r %r %r %r" % (parent_id, current_id, name_utf8,
2165
self._path_to_fileid_cache[cur_path] = file_id
2166
current_id = file_id
2170
"""Serialise the inventory to lines."""
2171
lines = ["chkinventory:\n"]
2172
if self._search_key_name != 'plain':
2173
# custom ordering grouping things that don't change together
2174
lines.append('search_key_name: %s\n' % (self._search_key_name,))
2175
lines.append("root_id: %s\n" % self.root_id)
2176
lines.append('parent_id_basename_to_file_id: %s\n' %
2177
(self.parent_id_basename_to_file_id.key()[0],))
2178
lines.append("revision_id: %s\n" % self.revision_id)
2179
lines.append("id_to_entry: %s\n" % (self.id_to_entry.key()[0],))
2181
lines.append("revision_id: %s\n" % self.revision_id)
2182
lines.append("root_id: %s\n" % self.root_id)
2183
if self.parent_id_basename_to_file_id is not None:
2184
lines.append('parent_id_basename_to_file_id: %s\n' %
2185
(self.parent_id_basename_to_file_id.key()[0],))
2186
lines.append("id_to_entry: %s\n" % (self.id_to_entry.key()[0],))
2191
"""Get the root entry."""
2192
return self[self.root_id]
2195
class CHKInventoryDirectory(InventoryDirectory):
2196
"""A directory in an inventory."""
2198
__slots__ = ['_children', '_chk_inventory']
2200
def __init__(self, file_id, name, parent_id, chk_inventory):
2201
# Don't call InventoryDirectory.__init__ - it isn't right for this
2203
InventoryEntry.__init__(self, file_id, name, parent_id)
2204
self._children = None
2205
self._chk_inventory = chk_inventory
2209
"""Access the list of children of this directory.
2211
With a parent_id_basename_to_file_id index, loads all the children,
2212
without loads the entire index. Without is bad. A more sophisticated
2213
proxy object might be nice, to allow partial loading of children as
2214
well when specific names are accessed. (So path traversal can be
2215
written in the obvious way but not examine siblings.).
2217
if self._children is not None:
2218
return self._children
2219
# No longer supported
2220
if self._chk_inventory.parent_id_basename_to_file_id is None:
2221
raise AssertionError("Inventories without"
2222
" parent_id_basename_to_file_id are no longer supported")
2224
# XXX: Todo - use proxy objects for the children rather than loading
2225
# all when the attribute is referenced.
2226
parent_id_index = self._chk_inventory.parent_id_basename_to_file_id
2228
for (parent_id, name_utf8), file_id in parent_id_index.iteritems(
2229
key_filter=[StaticTuple(self.file_id,)]):
2230
child_keys.add(StaticTuple(file_id,))
2232
for file_id_key in child_keys:
2233
entry = self._chk_inventory._fileid_to_entry_cache.get(
2234
file_id_key[0], None)
2235
if entry is not None:
2236
result[entry.name] = entry
2237
cached.add(file_id_key)
2238
child_keys.difference_update(cached)
2239
# populate; todo: do by name
2240
id_to_entry = self._chk_inventory.id_to_entry
2241
for file_id_key, bytes in id_to_entry.iteritems(child_keys):
2242
entry = self._chk_inventory._bytes_to_entry(bytes)
2243
result[entry.name] = entry
2244
self._chk_inventory._fileid_to_entry_cache[file_id_key[0]] = entry
2245
self._children = result
2249
'directory': InventoryDirectory,
2250
'file': InventoryFile,
2251
'symlink': InventoryLink,
2252
'tree-reference': TreeReference
2255
def make_entry(kind, name, parent_id, file_id=None):
2256
"""Create an inventory entry.
2258
:param kind: the type of inventory entry to create.
2259
:param name: the basename of the entry.
2260
:param parent_id: the parent_id of the entry.
2261
:param file_id: the file_id to use. if None, one will be created.
2264
file_id = generate_ids.gen_file_id(name)
2265
name = ensure_normalized_name(name)
2267
factory = entry_factory[kind]
2269
raise errors.BadFileKindError(name, kind)
2270
return factory(file_id, name, parent_id)
2273
def ensure_normalized_name(name):
2276
:raises InvalidNormalization: When name is not normalized, and cannot be
2277
accessed on this platform by the normalized path.
2278
:return: The NFC normalised version of name.
2280
#------- This has been copied to bzrlib.dirstate.DirState.add, please
2281
# keep them synchronised.
2282
# we dont import normalized_filename directly because we want to be
2283
# able to change the implementation at runtime for tests.
2284
norm_name, can_access = osutils.normalized_filename(name)
2285
if norm_name != name:
2289
# TODO: jam 20060701 This would probably be more useful
2290
# if the error was raised with the full path
2291
raise errors.InvalidNormalization(name)
2297
def is_valid_name(name):
2299
if _NAME_RE is None:
2300
_NAME_RE = re.compile(r'^[^/\\]+$')
2302
return bool(_NAME_RE.match(name))
2305
def _check_delta_unique_ids(delta):
2306
"""Decorate a delta and check that the file ids in it are unique.
2308
:return: A generator over delta.
2312
length = len(ids) + 1
2314
if len(ids) != length:
2315
raise errors.InconsistentDelta(item[0] or item[1], item[2],
2320
def _check_delta_unique_new_paths(delta):
2321
"""Decorate a delta and check that the new paths in it are unique.
2323
:return: A generator over delta.
2327
length = len(paths) + 1
2329
if path is not None:
2331
if len(paths) != length:
2332
raise errors.InconsistentDelta(path, item[2], "repeated path")
2336
def _check_delta_unique_old_paths(delta):
2337
"""Decorate a delta and check that the old paths in it are unique.
2339
:return: A generator over delta.
2343
length = len(paths) + 1
2345
if path is not None:
2347
if len(paths) != length:
2348
raise errors.InconsistentDelta(path, item[2], "repeated path")
2352
def _check_delta_ids_are_valid(delta):
2353
"""Decorate a delta and check that the ids in it are valid.
2355
:return: A generator over delta.
2360
raise errors.InconsistentDelta(item[0] or item[1], item[2],
2361
"entry with file_id None %r" % entry)
2362
if type(item[2]) != str:
2363
raise errors.InconsistentDelta(item[0] or item[1], item[2],
2364
"entry with non bytes file_id %r" % entry)
2368
def _check_delta_ids_match_entry(delta):
2369
"""Decorate a delta and check that the ids in it match the entry.file_id.
2371
:return: A generator over delta.
2375
if entry is not None:
2376
if entry.file_id != item[2]:
2377
raise errors.InconsistentDelta(item[0] or item[1], item[2],
2378
"mismatched id with %r" % entry)
2382
def _check_delta_new_path_entry_both_or_None(delta):
2383
"""Decorate a delta and check that the new_path and entry are paired.
2385
:return: A generator over delta.
2390
if new_path is None and entry is not None:
2391
raise errors.InconsistentDelta(item[0], item[1],
2392
"Entry with no new_path")
2393
if new_path is not None and entry is None:
2394
raise errors.InconsistentDelta(new_path, item[1],
2395
"new_path with no entry")
495
assert isinstance(file_id, str)
496
return self._byid.has_key(file_id)
500
if __name__ == '__main__':
501
import doctest, inventory
502
doctest.testmod(inventory)