1
# Copyright (C) 2005-2011 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
# FIXME: This refactoring of the workingtree code doesn't seem to keep
18
# the WorkingTree's copy of the inventory in sync with the branch. The
19
# branch modifies its working inventory when it does a commit to make
20
# missing files permanently removed.
22
# TODO: Maybe also keep the full path of the entry, and the children?
23
# But those depend on its position within a particular inventory, and
24
# it would be nice not to need to hold the backpointer here.
26
from __future__ import absolute_import
28
# This should really be an id randomly assigned when the tree is
29
# created, but it's not for now.
30
ROOT_ID = b"TREE_ROOT"
32
from ..lazy_import import lazy_import
33
lazy_import(globals(), """
40
from breezy.bzr import (
50
from ..sixish import (
56
from ..static_tuple import StaticTuple
59
class InventoryEntry(object):
60
"""Description of a versioned file.
62
An InventoryEntry has the following fields, which are also
63
present in the XML inventory-entry element:
68
(within the parent directory)
71
file_id of the parent directory, or ROOT_ID
74
the revision_id in which this variation of this file was
78
Indicates that this file should be executable on systems
82
sha-1 of the text of the file
85
size in bytes of the text of the file
87
(reading a version 4 tree created a text_id field.)
92
>>> i.add(InventoryDirectory(b'123', 'src', ROOT_ID))
93
InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None)
94
>>> i.add(InventoryFile(b'2323', 'hello.c', parent_id='123'))
95
InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None, revision=None)
96
>>> shouldbe = {0: '', 1: 'src', 2: 'src/hello.c'}
97
>>> for ix, j in enumerate(i.iter_entries()):
98
... print (j[0] == shouldbe[ix], j[1])
100
(True, InventoryDirectory('TREE_ROOT', u'', parent_id=None, revision=None))
101
(True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None))
102
(True, InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None, revision=None))
103
>>> i.add(InventoryFile('2324', 'bye.c', '123'))
104
InventoryFile('2324', 'bye.c', parent_id='123', sha1=None, len=None, revision=None)
105
>>> i.add(InventoryDirectory('2325', 'wibble', '123'))
106
InventoryDirectory('2325', 'wibble', parent_id='123', revision=None)
107
>>> i.path2id('src/wibble')
109
>>> i.add(InventoryFile('2326', 'wibble.c', '2325'))
110
InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None, revision=None)
111
>>> i.get_entry('2326')
112
InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None, revision=None)
113
>>> for path, entry in i.iter_entries():
122
>>> i.id2path(b'2326')
123
'src/wibble/wibble.c'
126
# Constants returned by describe_change()
128
# TODO: These should probably move to some kind of FileChangeDescription
129
# class; that's like what's inside a TreeDelta but we want to be able to
130
# generate them just for one file at a time.
132
MODIFIED_AND_RENAMED = 'modified and renamed'
134
__slots__ = ['file_id', 'revision', 'parent_id', 'name']
136
# Attributes that all InventoryEntry instances are expected to have, but
137
# that don't vary for all kinds of entry. (e.g. symlink_target is only
138
# relevant to InventoryLink, so there's no reason to make every
139
# InventoryFile instance allocate space to hold a value for it.)
140
# Attributes that only vary for files: executable, text_sha1, text_size,
146
# Attributes that only vary for symlinks: symlink_target
147
symlink_target = None
148
# Attributes that only vary for tree-references: reference_revision
149
reference_revision = None
151
def detect_changes(self, old_entry):
152
"""Return a (text_modified, meta_modified) from this to old_entry.
154
_read_tree_state must have been called on self and old_entry prior to
155
calling detect_changes.
159
def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
160
output_to, reverse=False):
161
"""Perform a diff between two entries of the same kind."""
163
def parent_candidates(self, previous_inventories):
164
"""Find possible per-file graph parents.
166
This is currently defined by:
167
- Select the last changed revision in the parent inventory.
168
- Do deal with a short lived bug in bzr 0.8's development two entries
169
that have the same last changed but different 'x' bit settings are
172
# revision:ie mapping for each ie found in previous_inventories.
174
# identify candidate head revision ids.
175
for inv in previous_inventories:
177
ie = inv.get_entry(self.file_id)
178
except errors.NoSuchId:
181
if ie.revision in candidates:
182
# same revision value in two different inventories:
183
# correct possible inconsistencies:
184
# * there was a bug in revision updates with 'x' bit
187
if candidates[ie.revision].executable != ie.executable:
188
candidates[ie.revision].executable = False
189
ie.executable = False
190
except AttributeError:
193
# add this revision as a candidate.
194
candidates[ie.revision] = ie
198
"""Return true if the object this entry represents has textual data.
200
Note that textual data includes binary content.
202
Also note that all entries get weave files created for them.
203
This attribute is primarily used when upgrading from old trees that
204
did not have the weave index for all inventory entries.
208
def __init__(self, file_id, name, parent_id):
209
"""Create an InventoryEntry
211
The filename must be a single component, relative to the
212
parent directory; it cannot be a whole path or relative name.
214
>>> e = InventoryFile(b'123', 'hello.c', ROOT_ID)
219
>>> e = InventoryFile(b'123', 'src/hello.c', ROOT_ID)
220
Traceback (most recent call last):
221
InvalidEntryName: Invalid entry name: src/hello.c
224
raise errors.InvalidEntryName(name=name)
225
if not isinstance(file_id, bytes):
226
raise TypeError(file_id)
227
self.file_id = file_id
230
self.parent_id = parent_id
232
def kind_character(self):
233
"""Return a short kind indicator useful for appending to names."""
234
raise errors.BzrError('unknown kind %r' % self.kind)
236
known_kinds = ('file', 'directory', 'symlink')
239
def versionable_kind(kind):
240
return (kind in ('file', 'directory', 'symlink', 'tree-reference'))
242
def check(self, checker, rev_id, inv):
243
"""Check this inventory entry is intact.
245
This is a template method, override _check for kind specific
248
:param checker: Check object providing context for the checks;
249
can be used to find out what parts of the repository have already
251
:param rev_id: Revision id from which this InventoryEntry was loaded.
252
Not necessarily the last-changed revision for this file.
253
:param inv: Inventory from which the entry was loaded.
255
if self.parent_id is not None:
256
if not inv.has_id(self.parent_id):
257
raise errors.BzrCheckError(
258
'missing parent {%s} in inventory for revision {%s}' % (
259
self.parent_id, rev_id))
260
checker._add_entry_to_text_key_references(inv, self)
261
self._check(checker, rev_id)
263
def _check(self, checker, rev_id):
264
"""Check this inventory entry for kind specific errors."""
265
checker._report_items.append(
266
'unknown entry kind %r in revision {%s}' % (self.kind, rev_id))
269
"""Clone this inventory entry."""
270
raise NotImplementedError
273
def describe_change(old_entry, new_entry):
274
"""Describe the change between old_entry and this.
276
This smells of being an InterInventoryEntry situation, but as its
277
the first one, we're making it a static method for now.
279
An entry with a different parent, or different name is considered
280
to be renamed. Reparenting is an internal detail.
281
Note that renaming the parent does not trigger a rename for the
284
# TODO: Perhaps return an object rather than just a string
285
if old_entry is new_entry:
286
# also the case of both being None
288
elif old_entry is None:
290
elif new_entry is None:
292
if old_entry.kind != new_entry.kind:
294
text_modified, meta_modified = new_entry.detect_changes(old_entry)
295
if text_modified or meta_modified:
299
# TODO 20060511 (mbp, rbc) factor out 'detect_rename' here.
300
if old_entry.parent_id != new_entry.parent_id:
302
elif old_entry.name != new_entry.name:
306
if renamed and not modified:
307
return InventoryEntry.RENAMED
308
if modified and not renamed:
310
if modified and renamed:
311
return InventoryEntry.MODIFIED_AND_RENAMED
315
return ("%s(%r, %r, parent_id=%r, revision=%r)"
316
% (self.__class__.__name__,
322
def __eq__(self, other):
324
# For the case when objects are cached
326
if not isinstance(other, InventoryEntry):
327
return NotImplemented
329
return ((self.file_id == other.file_id) and
330
(self.name == other.name) and
331
(other.symlink_target == self.symlink_target) and
332
(self.text_sha1 == other.text_sha1) and
333
(self.text_size == other.text_size) and
334
(self.text_id == other.text_id) and
335
(self.parent_id == other.parent_id) and
336
(self.kind == other.kind) and
337
(self.revision == other.revision) and
338
(self.executable == other.executable) and
339
(self.reference_revision == other.reference_revision)
342
def __ne__(self, other):
343
return not (self == other)
346
raise ValueError('not hashable')
348
def _unchanged(self, previous_ie):
349
"""Has this entry changed relative to previous_ie.
351
This method should be overridden in child classes.
354
# different inv parent
355
if previous_ie.parent_id != self.parent_id:
358
elif previous_ie.name != self.name:
360
elif previous_ie.kind != self.kind:
364
def _read_tree_state(self, path, work_tree):
365
"""Populate fields in the inventory entry from the given tree.
367
Note that this should be modified to be a noop on virtual trees
368
as all entries created there are prepopulated.
370
# TODO: Rather than running this manually, we should check the
371
# working sha1 and other expensive properties when they're
372
# first requested, or preload them if they're already known
373
pass # nothing to do by default
375
def _forget_tree_state(self):
379
class InventoryDirectory(InventoryEntry):
380
"""A directory in an inventory."""
382
__slots__ = ['children']
386
def _check(self, checker, rev_id):
387
"""See InventoryEntry._check"""
388
# In non rich root repositories we do not expect a file graph for the
390
if self.name == '' and not checker.rich_roots:
392
# Directories are stored as an empty file, but the file should exist
393
# to provide a per-fileid log. The hash of every directory content is
394
# "da..." below (the sha1sum of '').
395
checker.add_pending_item(rev_id,
396
('texts', self.file_id, self.revision), b'text',
397
b'da39a3ee5e6b4b0d3255bfef95601890afd80709')
400
other = InventoryDirectory(self.file_id, self.name, self.parent_id)
401
other.revision = self.revision
402
# note that children are *not* copied; they're pulled across when
406
def __init__(self, file_id, name, parent_id):
407
super(InventoryDirectory, self).__init__(file_id, name, parent_id)
410
def sorted_children(self):
411
return sorted(viewitems(self.children))
413
def kind_character(self):
414
"""See InventoryEntry.kind_character."""
418
class InventoryFile(InventoryEntry):
419
"""A file in an inventory."""
421
__slots__ = ['text_sha1', 'text_size', 'text_id', 'executable']
425
def __init__(self, file_id, name, parent_id):
426
super(InventoryFile, self).__init__(file_id, name, parent_id)
427
self.text_sha1 = None
428
self.text_size = None
430
self.executable = False
432
def _check(self, checker, tree_revision_id):
433
"""See InventoryEntry._check"""
434
# TODO: check size too.
435
checker.add_pending_item(tree_revision_id,
436
('texts', self.file_id, self.revision), b'text',
438
if self.text_size is None:
439
checker._report_items.append(
440
'fileid {%s} in {%s} has None for text_size' % (self.file_id,
444
other = InventoryFile(self.file_id, self.name, self.parent_id)
445
other.executable = self.executable
446
other.text_id = self.text_id
447
other.text_sha1 = self.text_sha1
448
other.text_size = self.text_size
449
other.revision = self.revision
452
def detect_changes(self, old_entry):
453
"""See InventoryEntry.detect_changes."""
454
text_modified = (self.text_sha1 != old_entry.text_sha1)
455
meta_modified = (self.executable != old_entry.executable)
456
return text_modified, meta_modified
458
def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
459
output_to, reverse=False):
460
"""See InventoryEntry._diff."""
461
from breezy.diff import DiffText
462
from_file_id = self.file_id
464
to_file_id = to_entry.file_id
465
to_path = to_tree.id2path(to_file_id)
469
if from_file_id is not None:
470
from_path = tree.id2path(from_file_id)
474
to_file_id, from_file_id = from_file_id, to_file_id
475
tree, to_tree = to_tree, tree
476
from_label, to_label = to_label, from_label
477
differ = DiffText(tree, to_tree, output_to, 'utf-8', '', '',
479
return differ.diff_text(from_path, to_path, from_label, to_label,
480
from_file_id, to_file_id)
483
"""See InventoryEntry.has_text."""
486
def kind_character(self):
487
"""See InventoryEntry.kind_character."""
490
def _read_tree_state(self, path, work_tree):
491
"""See InventoryEntry._read_tree_state."""
492
self.text_sha1 = work_tree.get_file_sha1(path, self.file_id)
493
# FIXME: 20050930 probe for the text size when getting sha1
494
# in _read_tree_state
495
self.executable = work_tree.is_executable(path, self.file_id)
498
return ("%s(%r, %r, parent_id=%r, sha1=%r, len=%s, revision=%s)"
499
% (self.__class__.__name__,
507
def _forget_tree_state(self):
508
self.text_sha1 = None
510
def _unchanged(self, previous_ie):
511
"""See InventoryEntry._unchanged."""
512
compatible = super(InventoryFile, self)._unchanged(previous_ie)
513
if self.text_sha1 != previous_ie.text_sha1:
516
# FIXME: 20050930 probe for the text size when getting sha1
517
# in _read_tree_state
518
self.text_size = previous_ie.text_size
519
if self.executable != previous_ie.executable:
524
class InventoryLink(InventoryEntry):
525
"""A file in an inventory."""
527
__slots__ = ['symlink_target']
531
def __init__(self, file_id, name, parent_id):
532
super(InventoryLink, self).__init__(file_id, name, parent_id)
533
self.symlink_target = None
535
def _check(self, checker, tree_revision_id):
536
"""See InventoryEntry._check"""
537
if self.symlink_target is None:
538
checker._report_items.append(
539
'symlink {%s} has no target in revision {%s}'
540
% (self.file_id, tree_revision_id))
541
# Symlinks are stored as ''
542
checker.add_pending_item(tree_revision_id,
543
('texts', self.file_id, self.revision), b'text',
544
b'da39a3ee5e6b4b0d3255bfef95601890afd80709')
547
other = InventoryLink(self.file_id, self.name, self.parent_id)
548
other.symlink_target = self.symlink_target
549
other.revision = self.revision
552
def detect_changes(self, old_entry):
553
"""See InventoryEntry.detect_changes."""
554
# FIXME: which _modified field should we use ? RBC 20051003
555
text_modified = (self.symlink_target != old_entry.symlink_target)
557
trace.mutter(" symlink target changed")
558
meta_modified = False
559
return text_modified, meta_modified
561
def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
562
output_to, reverse=False):
563
"""See InventoryEntry._diff."""
564
from breezy.diff import DiffSymlink
565
old_target = self.symlink_target
566
if to_entry is not None:
567
new_target = to_entry.symlink_target
576
new_target, old_target = old_target, new_target
577
differ = DiffSymlink(old_tree, new_tree, output_to)
578
return differ.diff_symlink(old_target, new_target)
580
def kind_character(self):
581
"""See InventoryEntry.kind_character."""
584
def _read_tree_state(self, path, work_tree):
585
"""See InventoryEntry._read_tree_state."""
586
self.symlink_target = work_tree.get_symlink_target(
587
work_tree.id2path(self.file_id), self.file_id)
589
def _forget_tree_state(self):
590
self.symlink_target = None
592
def _unchanged(self, previous_ie):
593
"""See InventoryEntry._unchanged."""
594
compatible = super(InventoryLink, self)._unchanged(previous_ie)
595
if self.symlink_target != previous_ie.symlink_target:
600
class TreeReference(InventoryEntry):
602
__slots__ = ['reference_revision']
604
kind = 'tree-reference'
606
def __init__(self, file_id, name, parent_id, revision=None,
607
reference_revision=None):
608
InventoryEntry.__init__(self, file_id, name, parent_id)
609
self.revision = revision
610
self.reference_revision = reference_revision
613
return TreeReference(self.file_id, self.name, self.parent_id,
614
self.revision, self.reference_revision)
616
def _read_tree_state(self, path, work_tree):
617
"""Populate fields in the inventory entry from the given tree.
619
self.reference_revision = work_tree.get_reference_revision(
622
def _forget_tree_state(self):
623
self.reference_revision = None
625
def _unchanged(self, previous_ie):
626
"""See InventoryEntry._unchanged."""
627
compatible = super(TreeReference, self)._unchanged(previous_ie)
628
if self.reference_revision != previous_ie.reference_revision:
632
def kind_character(self):
633
"""See InventoryEntry.kind_character."""
637
class CommonInventory(object):
638
"""Basic inventory logic, defined in terms of primitives like has_id.
640
An inventory is the metadata about the contents of a tree.
642
This is broadly a map from file_id to entries such as directories, files,
643
symlinks and tree references. Each entry maintains its own metadata like
644
SHA1 and length for files, or children for a directory.
646
Entries can be looked up either by path or by file_id.
648
InventoryEntry objects must not be modified after they are
649
inserted, other than through the Inventory API.
652
def has_filename(self, filename):
653
return bool(self.path2id(filename))
655
def id2path(self, file_id):
656
"""Return as a string the path to file_id.
659
>>> e = i.add(InventoryDirectory(b'src-id', 'src', ROOT_ID))
660
>>> e = i.add(InventoryFile(b'foo-id', 'foo.c', parent_id='src-id'))
661
>>> print i.id2path(b'foo-id')
664
:raises NoSuchId: If file_id is not present in the inventory.
666
# get all names, skipping root
667
return '/'.join(reversed(
668
[parent.name for parent in
669
self._iter_file_id_parents(file_id)][:-1]))
671
def iter_entries(self, from_dir=None, recursive=True):
672
"""Return (path, entry) pairs, in order by name.
674
:param from_dir: if None, start from the root,
675
otherwise start from this directory (either file-id or entry)
676
:param recursive: recurse into directories or not
679
if self.root is None:
683
elif isinstance(from_dir, bytes):
684
from_dir = self.get_entry(from_dir)
686
# unrolling the recursive called changed the time from
687
# 440ms/663ms (inline/total) to 116ms/116ms
688
children = sorted(viewitems(from_dir.children))
690
for name, ie in children:
693
children = collections.deque(children)
694
stack = [(u'', children)]
696
from_dir_relpath, children = stack[-1]
699
name, ie = children.popleft()
701
# we know that from_dir_relpath never ends in a slash
702
# and 'f' doesn't begin with one, we can do a string op, rather
703
# than the checks of pathjoin(), though this means that all paths
705
path = from_dir_relpath + '/' + name
709
if ie.kind != 'directory':
712
# But do this child first
713
new_children = sorted(viewitems(ie.children))
714
new_children = collections.deque(new_children)
715
stack.append((path, new_children))
716
# Break out of inner loop, so that we start outer loop with child
719
# if we finished all children, pop it off the stack
722
def _preload_cache(self):
723
"""Populate any caches, we are about to access all items.
725
The default implementation does nothing, because CommonInventory doesn't
730
def iter_entries_by_dir(self, from_dir=None, specific_file_ids=None):
731
"""Iterate over the entries in a directory first order.
733
This returns all entries for a directory before returning
734
the entries for children of a directory. This is not
735
lexicographically sorted order, and is a hybrid between
736
depth-first and breadth-first.
738
:return: This yields (path, entry) pairs
740
if specific_file_ids and not isinstance(specific_file_ids, set):
741
specific_file_ids = set(specific_file_ids)
742
# TODO? Perhaps this should return the from_dir so that the root is
743
# yielded? or maybe an option?
744
if from_dir is None and specific_file_ids is None:
745
# They are iterating from the root, and have not specified any
746
# specific entries to look at. All current callers fully consume the
747
# iterator, so we can safely assume we are accessing all entries
748
self._preload_cache()
750
if self.root is None:
752
# Optimize a common case
753
if (specific_file_ids is not None
754
and len(specific_file_ids) == 1):
755
file_id = list(specific_file_ids)[0]
756
if file_id is not None:
758
path = self.id2path(file_id)
759
except errors.NoSuchId:
762
yield path, self.get_entry(file_id)
765
if (specific_file_ids is None
766
or self.root.file_id in specific_file_ids):
768
elif isinstance(from_dir, bytes):
769
from_dir = self.get_entry(from_dir)
771
raise TypeError(from_dir)
773
if specific_file_ids is not None:
774
# TODO: jam 20070302 This could really be done as a loop rather
775
# than a bunch of recursive calls.
779
def add_ancestors(file_id):
780
if not byid.has_id(file_id):
782
parent_id = byid.get_entry(file_id).parent_id
783
if parent_id is None:
785
if parent_id not in parents:
786
parents.add(parent_id)
787
add_ancestors(parent_id)
788
for file_id in specific_file_ids:
789
add_ancestors(file_id)
793
stack = [(u'', from_dir)]
795
cur_relpath, cur_dir = stack.pop()
798
for child_name, child_ie in sorted(viewitems(cur_dir.children)):
800
child_relpath = cur_relpath + child_name
802
if (specific_file_ids is None
803
or child_ie.file_id in specific_file_ids):
804
yield child_relpath, child_ie
806
if child_ie.kind == 'directory':
807
if parents is None or child_ie.file_id in parents:
808
child_dirs.append((child_relpath + '/', child_ie))
809
stack.extend(reversed(child_dirs))
811
def _make_delta(self, old):
812
"""Make an inventory delta from two inventories."""
813
old_ids = set(old.iter_all_ids())
814
new_ids = set(self.iter_all_ids())
815
adds = new_ids - old_ids
816
deletes = old_ids - new_ids
817
common = old_ids.intersection(new_ids)
819
for file_id in deletes:
820
delta.append((old.id2path(file_id), None, file_id, None))
822
delta.append((None, self.id2path(file_id),
823
file_id, self.get_entry(file_id)))
824
for file_id in common:
825
if old.get_entry(file_id) != self.get_entry(file_id):
826
delta.append((old.id2path(file_id), self.id2path(file_id),
827
file_id, self.get_entry(file_id)))
830
def make_entry(self, kind, name, parent_id, file_id=None):
831
"""Simple thunk to breezy.bzr.inventory.make_entry."""
832
return make_entry(kind, name, parent_id, file_id)
835
"""Return list of (path, ie) for all entries except the root.
837
This may be faster than iter_entries.
841
def descend(dir_ie, dir_path):
842
kids = sorted(viewitems(dir_ie.children))
843
for name, ie in kids:
844
child_path = osutils.pathjoin(dir_path, name)
845
accum.append((child_path, ie))
846
if ie.kind == 'directory':
847
descend(ie, child_path)
849
if self.root is not None:
850
descend(self.root, u'')
853
def get_entry_by_path(self, relpath):
854
"""Return an inventory entry by path.
856
:param relpath: may be either a list of path components, or a single
857
string, in which case it is automatically split.
859
This returns the entry of the last component in the path,
860
which may be either a file or a directory.
862
Returns None IFF the path is not found.
864
if isinstance(relpath, (str, text_type)):
865
names = osutils.splitpath(relpath)
871
except errors.NoSuchId:
872
# root doesn't exist yet so nothing else can
878
children = getattr(parent, 'children', None)
888
def path2id(self, relpath):
889
"""Walk down through directories to return entry of last component.
891
:param relpath: may be either a list of path components, or a single
892
string, in which case it is automatically split.
894
This returns the entry of the last component in the path,
895
which may be either a file or a directory.
897
Returns None IFF the path is not found.
899
ie = self.get_entry_by_path(relpath)
904
def filter(self, specific_fileids):
905
"""Get an inventory view filtered against a set of file-ids.
907
Children of directories and parents are included.
909
The result may or may not reference the underlying inventory
910
so it should be treated as immutable.
912
interesting_parents = set()
913
for fileid in specific_fileids:
915
interesting_parents.update(self.get_idpath(fileid))
916
except errors.NoSuchId:
917
# This fileid is not in the inventory - that's ok
919
entries = self.iter_entries()
920
if self.root is None:
921
return Inventory(root_id=None)
922
other = Inventory(next(entries)[1].file_id)
923
other.root.revision = self.root.revision
924
other.revision_id = self.revision_id
925
directories_to_expand = set()
926
for path, entry in entries:
927
file_id = entry.file_id
928
if (file_id in specific_fileids or
929
entry.parent_id in directories_to_expand):
930
if entry.kind == 'directory':
931
directories_to_expand.add(file_id)
932
elif file_id not in interesting_parents:
934
other.add(entry.copy())
937
def get_idpath(self, file_id):
938
"""Return a list of file_ids for the path to an entry.
940
The list contains one element for each directory followed by
941
the id of the file itself. So the length of the returned list
942
is equal to the depth of the file in the tree, counting the
943
root directory as depth 1.
946
for parent in self._iter_file_id_parents(file_id):
947
p.insert(0, parent.file_id)
951
class Inventory(CommonInventory):
952
"""Mutable dict based in-memory inventory.
954
We never store the full path to a file, because renaming a directory
955
implicitly moves all of its contents. This class internally maintains a
956
lookup tree that allows the children under a directory to be
959
>>> inv = Inventory()
960
>>> inv.add(InventoryFile(b'123-123', 'hello.c', ROOT_ID))
961
InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
962
>>> inv.get_entry(b'123-123').name
965
Id's may be looked up from paths:
967
>>> inv.path2id('hello.c')
969
>>> inv.has_id(b'123-123')
972
There are iterators over the contents:
974
>>> [entry[0] for entry in inv.iter_entries()]
978
def __init__(self, root_id=ROOT_ID, revision_id=None):
979
"""Create or read an inventory.
981
If a working directory is specified, the inventory is read
982
from there. If the file is specified, read from that. If not,
983
the inventory is created empty.
985
The inventory is created with a default root directory, with
988
if root_id is not None:
989
self._set_root(InventoryDirectory(root_id, u'', None))
993
self.revision_id = revision_id
996
# More than one page of ouput is not useful anymore to debug
999
contents = repr(self._byid)
1000
if len(contents) > max_len:
1001
contents = contents[:(max_len - len(closing))] + closing
1002
return "<Inventory object at %x, contents=%r>" % (id(self), contents)
1004
def apply_delta(self, delta):
1005
"""Apply a delta to this inventory.
1007
See the inventory developers documentation for the theory behind
1010
If delta application fails the inventory is left in an indeterminate
1011
state and must not be used.
1013
:param delta: A list of changes to apply. After all the changes are
1014
applied the final inventory must be internally consistent, but it
1015
is ok to supply changes which, if only half-applied would have an
1016
invalid result - such as supplying two changes which rename two
1017
files, 'A' and 'B' with each other : [('A', 'B', b'A-id', a_entry),
1018
('B', 'A', b'B-id', b_entry)].
1020
Each change is a tuple, of the form (old_path, new_path, file_id,
1023
When new_path is None, the change indicates the removal of an entry
1024
from the inventory and new_entry will be ignored (using None is
1025
appropriate). If new_path is not None, then new_entry must be an
1026
InventoryEntry instance, which will be incorporated into the
1027
inventory (and replace any existing entry with the same file id).
1029
When old_path is None, the change indicates the addition of
1030
a new entry to the inventory.
1032
When neither new_path nor old_path are None, the change is a
1033
modification to an entry, such as a rename, reparent, kind change
1036
The children attribute of new_entry is ignored. This is because
1037
this method preserves children automatically across alterations to
1038
the parent of the children, and cases where the parent id of a
1039
child is changing require the child to be passed in as a separate
1040
change regardless. E.g. in the recursive deletion of a directory -
1041
the directory's children must be included in the delta, or the
1042
final inventory will be invalid.
1044
Note that a file_id must only appear once within a given delta.
1045
An AssertionError is raised otherwise.
1047
# Check that the delta is legal. It would be nice if this could be
1048
# done within the loops below but it's safer to validate the delta
1049
# before starting to mutate the inventory, as there isn't a rollback
1051
list(_check_delta_unique_ids(_check_delta_unique_new_paths(
1052
_check_delta_unique_old_paths(_check_delta_ids_match_entry(
1053
_check_delta_ids_are_valid(
1054
_check_delta_new_path_entry_both_or_None(
1058
# Remove all affected items which were in the original inventory,
1059
# starting with the longest paths, thus ensuring parents are examined
1060
# after their children, which means that everything we examine has no
1061
# modified children remaining by the time we examine it.
1062
for old_path, file_id in sorted(((op, f) for op, np, f, e in delta
1063
if op is not None), reverse=True):
1064
# Preserve unaltered children of file_id for later reinsertion.
1065
file_id_children = getattr(self.get_entry(file_id), 'children', {})
1066
if len(file_id_children):
1067
children[file_id] = file_id_children
1068
if self.id2path(file_id) != old_path:
1069
raise errors.InconsistentDelta(old_path, file_id,
1070
"Entry was at wrong other path %r." % self.id2path(file_id))
1071
# Remove file_id and the unaltered children. If file_id is not
1072
# being deleted it will be reinserted back later.
1073
self.remove_recursive_id(file_id)
1074
# Insert all affected which should be in the new inventory, reattaching
1075
# their children if they had any. This is done from shortest path to
1076
# longest, ensuring that items which were modified and whose parents in
1077
# the resulting inventory were also modified, are inserted after their
1079
for new_path, f, new_entry in sorted((np, f, e) for op, np, f, e in
1080
delta if np is not None):
1081
if new_entry.kind == 'directory':
1082
# Pop the child which to allow detection of children whose
1083
# parents were deleted and which were not reattached to a new
1085
replacement = InventoryDirectory(new_entry.file_id,
1086
new_entry.name, new_entry.parent_id)
1087
replacement.revision = new_entry.revision
1088
replacement.children = children.pop(replacement.file_id, {})
1089
new_entry = replacement
1092
except errors.DuplicateFileId:
1093
raise errors.InconsistentDelta(new_path, new_entry.file_id,
1094
"New id is already present in target.")
1095
except AttributeError:
1096
raise errors.InconsistentDelta(new_path, new_entry.file_id,
1097
"Parent is not a directory.")
1098
if self.id2path(new_entry.file_id) != new_path:
1099
raise errors.InconsistentDelta(new_path, new_entry.file_id,
1100
"New path is not consistent with parent path.")
1102
# Get the parent id that was deleted
1103
parent_id, children = children.popitem()
1104
raise errors.InconsistentDelta("<deleted>", parent_id,
1105
"The file id was deleted but its children were not deleted.")
1107
def create_by_apply_delta(self, inventory_delta, new_revision_id,
1108
propagate_caches=False):
1109
"""See CHKInventory.create_by_apply_delta()"""
1110
new_inv = self.copy()
1111
new_inv.apply_delta(inventory_delta)
1112
new_inv.revision_id = new_revision_id
1115
def _set_root(self, ie):
1117
self._byid = {self.root.file_id: self.root}
1120
# TODO: jam 20051218 Should copy also copy the revision_id?
1121
entries = self.iter_entries()
1122
if self.root is None:
1123
return Inventory(root_id=None)
1124
other = Inventory(next(entries)[1].file_id)
1125
other.root.revision = self.root.revision
1126
# copy recursively so we know directories will be added before
1127
# their children. There are more efficient ways than this...
1128
for path, entry in entries:
1129
other.add(entry.copy())
1132
def iter_all_ids(self):
1133
"""Iterate over all file-ids."""
1134
return iter(self._byid)
1136
def iter_just_entries(self):
1137
"""Iterate over all entries.
1139
Unlike iter_entries(), just the entries are returned (not (path, ie))
1140
and the order of entries is undefined.
1142
XXX: We may not want to merge this into bzr.dev.
1144
if self.root is None:
1146
return iter(viewvalues(self._byid))
1149
"""Returns number of entries."""
1150
return len(self._byid)
1152
def get_entry(self, file_id):
1153
"""Return the entry for given file_id.
1155
>>> inv = Inventory()
1156
>>> inv.add(InventoryFile(b'123123', 'hello.c', ROOT_ID))
1157
InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
1158
>>> inv.get_entry(b'123123').name
1161
if not isinstance(file_id, bytes):
1162
raise TypeError(file_id)
1164
return self._byid[file_id]
1166
# really we're passing an inventory, not a tree...
1167
raise errors.NoSuchId(self, file_id)
1169
def get_file_kind(self, file_id):
1170
return self._byid[file_id].kind
1172
def get_child(self, parent_id, filename):
1173
return self.get_entry(parent_id).children.get(filename)
1175
def _add_child(self, entry):
1176
"""Add an entry to the inventory, without adding it to its parent"""
1177
if entry.file_id in self._byid:
1178
raise errors.BzrError(
1179
"inventory already contains entry with id {%s}" %
1181
self._byid[entry.file_id] = entry
1182
children = getattr(entry, 'children', {})
1183
if children is not None:
1184
for child in viewvalues(children):
1185
self._add_child(child)
1188
def add(self, entry):
1189
"""Add entry to inventory.
1193
if entry.file_id in self._byid:
1194
raise errors.DuplicateFileId(entry.file_id,
1195
self._byid[entry.file_id])
1196
if entry.parent_id is None:
1200
parent = self._byid[entry.parent_id]
1202
raise errors.InconsistentDelta("<unknown>", entry.parent_id,
1203
"Parent not in inventory.")
1204
if entry.name in parent.children:
1205
raise errors.InconsistentDelta(
1206
self.id2path(parent.children[entry.name].file_id),
1208
"Path already versioned")
1209
parent.children[entry.name] = entry
1210
return self._add_child(entry)
1212
def add_path(self, relpath, kind, file_id=None, parent_id=None):
1213
"""Add entry from a path.
1215
The immediate parent must already be versioned.
1217
Returns the new entry object."""
1219
parts = osutils.splitpath(relpath)
1223
file_id = generate_ids.gen_root_id()
1224
self.root = InventoryDirectory(file_id, '', None)
1225
self._byid = {self.root.file_id: self.root}
1228
parent_path = parts[:-1]
1229
parent_id = self.path2id(parent_path)
1230
if parent_id is None:
1231
raise errors.NotVersionedError(path=parent_path)
1232
ie = make_entry(kind, parts[-1], parent_id, file_id)
1235
def delete(self, file_id):
1236
"""Remove entry by id.
1238
>>> inv = Inventory()
1239
>>> inv.add(InventoryFile(b'123', 'foo.c', ROOT_ID))
1240
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
1241
>>> inv.has_id(b'123')
1243
>>> inv.delete(b'123')
1244
>>> inv.has_id(b'123')
1247
ie = self.get_entry(file_id)
1248
del self._byid[file_id]
1249
if ie.parent_id is not None:
1250
del self.get_entry(ie.parent_id).children[ie.name]
1252
def __eq__(self, other):
1253
"""Compare two sets by comparing their contents.
1255
>>> i1 = Inventory()
1256
>>> i2 = Inventory()
1259
>>> i1.add(InventoryFile(b'123', 'foo', ROOT_ID))
1260
InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
1263
>>> i2.add(InventoryFile(b'123', 'foo', ROOT_ID))
1264
InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
1268
if not isinstance(other, Inventory):
1269
return NotImplemented
1271
return self._byid == other._byid
1273
def __ne__(self, other):
1274
return not self.__eq__(other)
1277
raise ValueError('not hashable')
1279
def _iter_file_id_parents(self, file_id):
1280
"""Yield the parents of file_id up to the root."""
1281
while file_id is not None:
1283
ie = self._byid[file_id]
1285
raise errors.NoSuchId(tree=None, file_id=file_id)
1287
file_id = ie.parent_id
1289
def has_id(self, file_id):
1290
return (file_id in self._byid)
1292
def _make_delta(self, old):
1293
"""Make an inventory delta from two inventories."""
1294
old_getter = old.get_entry
1295
new_getter = self.get_entry
1296
old_ids = set(old.iter_all_ids())
1297
new_ids = set(self.iter_all_ids())
1298
adds = new_ids - old_ids
1299
deletes = old_ids - new_ids
1300
if not adds and not deletes:
1303
common = old_ids.intersection(new_ids)
1305
for file_id in deletes:
1306
delta.append((old.id2path(file_id), None, file_id, None))
1307
for file_id in adds:
1308
delta.append((None, self.id2path(file_id),
1309
file_id, self.get_entry(file_id)))
1310
for file_id in common:
1311
new_ie = new_getter(file_id)
1312
old_ie = old_getter(file_id)
1313
# If xml_serializer returns the cached InventoryEntries (rather
1314
# than always doing .copy()), inlining the 'is' check saves 2.7M
1315
# calls to __eq__. Under lsprof this saves 20s => 6s.
1316
# It is a minor improvement without lsprof.
1317
if old_ie is new_ie or old_ie == new_ie:
1320
delta.append((old.id2path(file_id), self.id2path(file_id),
1324
def remove_recursive_id(self, file_id):
1325
"""Remove file_id, and children, from the inventory.
1327
:param file_id: A file_id to remove.
1329
to_find_delete = [self._byid[file_id]]
1331
while to_find_delete:
1332
ie = to_find_delete.pop()
1333
to_delete.append(ie.file_id)
1334
if ie.kind == 'directory':
1335
to_find_delete.extend(viewvalues(ie.children))
1336
for file_id in reversed(to_delete):
1337
ie = self.get_entry(file_id)
1338
del self._byid[file_id]
1339
if ie.parent_id is not None:
1340
del self.get_entry(ie.parent_id).children[ie.name]
1344
def rename(self, file_id, new_parent_id, new_name):
1345
"""Move a file within the inventory.
1347
This can change either the name, or the parent, or both.
1349
This does not move the working file.
1351
new_name = ensure_normalized_name(new_name)
1352
if not is_valid_name(new_name):
1353
raise errors.BzrError("not an acceptable filename: %r" % new_name)
1355
new_parent = self._byid[new_parent_id]
1356
if new_name in new_parent.children:
1357
raise errors.BzrError("%r already exists in %r" %
1358
(new_name, self.id2path(new_parent_id)))
1360
new_parent_idpath = self.get_idpath(new_parent_id)
1361
if file_id in new_parent_idpath:
1362
raise errors.BzrError(
1363
"cannot move directory %r into a subdirectory of itself, %r"
1364
% (self.id2path(file_id), self.id2path(new_parent_id)))
1366
file_ie = self._byid[file_id]
1367
old_parent = self._byid[file_ie.parent_id]
1369
# TODO: Don't leave things messed up if this fails
1371
del old_parent.children[file_ie.name]
1372
new_parent.children[new_name] = file_ie
1374
file_ie.name = new_name
1375
file_ie.parent_id = new_parent_id
1377
def is_root(self, file_id):
1378
return self.root is not None and file_id == self.root.file_id
1381
class CHKInventory(CommonInventory):
1382
"""An inventory persisted in a CHK store.
1384
By design, a CHKInventory is immutable so many of the methods
1385
supported by Inventory - add, rename, apply_delta, etc - are *not*
1386
supported. To create a new CHKInventory, use create_by_apply_delta()
1387
or from_inventory(), say.
1389
Internally, a CHKInventory has one or two CHKMaps:
1391
* id_to_entry - a map from (file_id,) => InventoryEntry as bytes
1392
* parent_id_basename_to_file_id - a map from (parent_id, basename_utf8)
1395
The second map is optional and not present in early CHkRepository's.
1397
No caching is performed: every method call or item access will perform
1398
requests to the storage layer. As such, keep references to objects you
1402
def __init__(self, search_key_name):
1403
CommonInventory.__init__(self)
1404
self._fileid_to_entry_cache = {}
1405
self._fully_cached = False
1406
self._path_to_fileid_cache = {}
1407
self._search_key_name = search_key_name
1410
def __eq__(self, other):
1411
"""Compare two sets by comparing their contents."""
1412
if not isinstance(other, CHKInventory):
1413
return NotImplemented
1415
this_key = self.id_to_entry.key()
1416
other_key = other.id_to_entry.key()
1417
this_pid_key = self.parent_id_basename_to_file_id.key()
1418
other_pid_key = other.parent_id_basename_to_file_id.key()
1419
if None in (this_key, this_pid_key, other_key, other_pid_key):
1421
return this_key == other_key and this_pid_key == other_pid_key
1423
def _entry_to_bytes(self, entry):
1424
"""Serialise entry as a single bytestring.
1426
:param Entry: An inventory entry.
1427
:return: A bytestring for the entry.
1430
ENTRY ::= FILE | DIR | SYMLINK | TREE
1431
FILE ::= "file: " COMMON SEP SHA SEP SIZE SEP EXECUTABLE
1432
DIR ::= "dir: " COMMON
1433
SYMLINK ::= "symlink: " COMMON SEP TARGET_UTF8
1434
TREE ::= "tree: " COMMON REFERENCE_REVISION
1435
COMMON ::= FILE_ID SEP PARENT_ID SEP NAME_UTF8 SEP REVISION
1438
if entry.parent_id is not None:
1439
parent_str = entry.parent_id
1442
name_str = entry.name.encode("utf8")
1443
if entry.kind == 'file':
1444
if entry.executable:
1448
return b"file: %s\n%s\n%s\n%s\n%s\n%d\n%s" % (
1449
entry.file_id, parent_str, name_str, entry.revision,
1450
entry.text_sha1, entry.text_size, exec_str)
1451
elif entry.kind == 'directory':
1452
return b"dir: %s\n%s\n%s\n%s" % (
1453
entry.file_id, parent_str, name_str, entry.revision)
1454
elif entry.kind == 'symlink':
1455
return b"symlink: %s\n%s\n%s\n%s\n%s" % (
1456
entry.file_id, parent_str, name_str, entry.revision,
1457
entry.symlink_target.encode("utf8"))
1458
elif entry.kind == 'tree-reference':
1459
return b"tree: %s\n%s\n%s\n%s\n%s" % (
1460
entry.file_id, parent_str, name_str, entry.revision,
1461
entry.reference_revision)
1463
raise ValueError("unknown kind %r" % entry.kind)
1465
def _expand_fileids_to_parents_and_children(self, file_ids):
1466
"""Give a more wholistic view starting with the given file_ids.
1468
For any file_id which maps to a directory, we will include all children
1469
of that directory. We will also include all directories which are
1470
parents of the given file_ids, but we will not include their children.
1477
fringle # fringle-id
1481
if given [foo-id] we will include
1482
TREE_ROOT as interesting parents
1484
foo-id, baz-id, frob-id, fringle-id
1488
# TODO: Pre-pass over the list of fileids to see if anything is already
1489
# deserialized in self._fileid_to_entry_cache
1491
directories_to_expand = set()
1492
children_of_parent_id = {}
1493
# It is okay if some of the fileids are missing
1494
for entry in self._getitems(file_ids):
1495
if entry.kind == 'directory':
1496
directories_to_expand.add(entry.file_id)
1497
interesting.add(entry.parent_id)
1498
children_of_parent_id.setdefault(entry.parent_id, set()
1499
).add(entry.file_id)
1501
# Now, interesting has all of the direct parents, but not the
1502
# parents of those parents. It also may have some duplicates with
1504
remaining_parents = interesting.difference(file_ids)
1505
# When we hit the TREE_ROOT, we'll get an interesting parent of None,
1506
# but we don't actually want to recurse into that
1507
interesting.add(None) # this will auto-filter it in the loop
1508
remaining_parents.discard(None)
1509
while remaining_parents:
1510
next_parents = set()
1511
for entry in self._getitems(remaining_parents):
1512
next_parents.add(entry.parent_id)
1513
children_of_parent_id.setdefault(entry.parent_id, set()
1514
).add(entry.file_id)
1515
# Remove any search tips we've already processed
1516
remaining_parents = next_parents.difference(interesting)
1517
interesting.update(remaining_parents)
1518
# We should probably also .difference(directories_to_expand)
1519
interesting.update(file_ids)
1520
interesting.discard(None)
1521
while directories_to_expand:
1522
# Expand directories by looking in the
1523
# parent_id_basename_to_file_id map
1524
keys = [StaticTuple(f,).intern() for f in directories_to_expand]
1525
directories_to_expand = set()
1526
items = self.parent_id_basename_to_file_id.iteritems(keys)
1527
next_file_ids = {item[1] for item in items}
1528
next_file_ids = next_file_ids.difference(interesting)
1529
interesting.update(next_file_ids)
1530
for entry in self._getitems(next_file_ids):
1531
if entry.kind == 'directory':
1532
directories_to_expand.add(entry.file_id)
1533
children_of_parent_id.setdefault(entry.parent_id, set()
1534
).add(entry.file_id)
1535
return interesting, children_of_parent_id
1537
def filter(self, specific_fileids):
1538
"""Get an inventory view filtered against a set of file-ids.
1540
Children of directories and parents are included.
1542
The result may or may not reference the underlying inventory
1543
so it should be treated as immutable.
1546
parent_to_children) = self._expand_fileids_to_parents_and_children(
1548
# There is some overlap here, but we assume that all interesting items
1549
# are in the _fileid_to_entry_cache because we had to read them to
1550
# determine if they were a dir we wanted to recurse, or just a file
1551
# This should give us all the entries we'll want to add, so start
1553
other = Inventory(self.root_id)
1554
other.root.revision = self.root.revision
1555
other.revision_id = self.revision_id
1556
if not interesting or not parent_to_children:
1557
# empty filter, or filtering entrys that don't exist
1558
# (if even 1 existed, then we would have populated
1559
# parent_to_children with at least the tree root.)
1561
cache = self._fileid_to_entry_cache
1562
remaining_children = collections.deque(
1563
parent_to_children[self.root_id])
1564
while remaining_children:
1565
file_id = remaining_children.popleft()
1567
if ie.kind == 'directory':
1568
ie = ie.copy() # We create a copy to depopulate the .children attribute
1569
# TODO: depending on the uses of 'other' we should probably alwyas
1570
# '.copy()' to prevent someone from mutating other and
1571
# invaliding our internal cache
1573
if file_id in parent_to_children:
1574
remaining_children.extend(parent_to_children[file_id])
1578
def _bytes_to_utf8name_key(data):
1579
"""Get the file_id, revision_id key out of data."""
1580
# We don't normally care about name, except for times when we want
1581
# to filter out empty names because of non rich-root...
1582
sections = data.split(b'\n')
1583
kind, file_id = sections[0].split(b': ')
1584
return (sections[2], bytesintern(file_id), bytesintern(sections[3]))
1586
def _bytes_to_entry(self, bytes):
1587
"""Deserialise a serialised entry."""
1588
sections = bytes.split(b'\n')
1589
if sections[0].startswith(b"file: "):
1590
result = InventoryFile(sections[0][6:],
1591
sections[2].decode('utf8'),
1593
result.text_sha1 = sections[4]
1594
result.text_size = int(sections[5])
1595
result.executable = sections[6] == b"Y"
1596
elif sections[0].startswith(b"dir: "):
1597
result = CHKInventoryDirectory(sections[0][5:],
1598
sections[2].decode('utf8'),
1600
elif sections[0].startswith(b"symlink: "):
1601
result = InventoryLink(sections[0][9:],
1602
sections[2].decode('utf8'),
1604
result.symlink_target = sections[4].decode('utf8')
1605
elif sections[0].startswith(b"tree: "):
1606
result = TreeReference(sections[0][6:],
1607
sections[2].decode('utf8'),
1609
result.reference_revision = sections[4]
1611
raise ValueError("Not a serialised entry %r" % bytes)
1612
result.file_id = bytesintern(result.file_id)
1613
result.revision = bytesintern(sections[3])
1614
if result.parent_id == b'':
1615
result.parent_id = None
1616
self._fileid_to_entry_cache[result.file_id] = result
1619
def create_by_apply_delta(self, inventory_delta, new_revision_id,
1620
propagate_caches=False):
1621
"""Create a new CHKInventory by applying inventory_delta to this one.
1623
See the inventory developers documentation for the theory behind
1626
:param inventory_delta: The inventory delta to apply. See
1627
Inventory.apply_delta for details.
1628
:param new_revision_id: The revision id of the resulting CHKInventory.
1629
:param propagate_caches: If True, the caches for this inventory are
1630
copied to and updated for the result.
1631
:return: The new CHKInventory.
1633
split = osutils.split
1634
result = CHKInventory(self._search_key_name)
1635
if propagate_caches:
1636
# Just propagate the path-to-fileid cache for now
1637
result._path_to_fileid_cache = self._path_to_fileid_cache.copy()
1638
search_key_func = chk_map.search_key_registry.get(
1639
self._search_key_name)
1640
self.id_to_entry._ensure_root()
1641
maximum_size = self.id_to_entry._root_node.maximum_size
1642
result.revision_id = new_revision_id
1643
result.id_to_entry = chk_map.CHKMap(
1644
self.id_to_entry._store,
1645
self.id_to_entry.key(),
1646
search_key_func=search_key_func)
1647
result.id_to_entry._ensure_root()
1648
result.id_to_entry._root_node.set_maximum_size(maximum_size)
1649
# Change to apply to the parent_id_basename delta. The dict maps
1650
# (parent_id, basename) -> (old_key, new_value). We use a dict because
1651
# when a path has its id replaced (e.g. the root is changed, or someone
1652
# does bzr mv a b, bzr mv c a, we should output a single change to this
1653
# map rather than two.
1654
parent_id_basename_delta = {}
1655
if self.parent_id_basename_to_file_id is not None:
1656
result.parent_id_basename_to_file_id = chk_map.CHKMap(
1657
self.parent_id_basename_to_file_id._store,
1658
self.parent_id_basename_to_file_id.key(),
1659
search_key_func=search_key_func)
1660
result.parent_id_basename_to_file_id._ensure_root()
1661
self.parent_id_basename_to_file_id._ensure_root()
1662
result_p_id_root = result.parent_id_basename_to_file_id._root_node
1663
p_id_root = self.parent_id_basename_to_file_id._root_node
1664
result_p_id_root.set_maximum_size(p_id_root.maximum_size)
1665
result_p_id_root._key_width = p_id_root._key_width
1667
result.parent_id_basename_to_file_id = None
1668
result.root_id = self.root_id
1669
id_to_entry_delta = []
1670
# inventory_delta is only traversed once, so we just update the
1672
# Check for repeated file ids
1673
inventory_delta = _check_delta_unique_ids(inventory_delta)
1674
# Repeated old paths
1675
inventory_delta = _check_delta_unique_old_paths(inventory_delta)
1676
# Check for repeated new paths
1677
inventory_delta = _check_delta_unique_new_paths(inventory_delta)
1678
# Check for entries that don't match the fileid
1679
inventory_delta = _check_delta_ids_match_entry(inventory_delta)
1680
# Check for nonsense fileids
1681
inventory_delta = _check_delta_ids_are_valid(inventory_delta)
1682
# Check for new_path <-> entry consistency
1683
inventory_delta = _check_delta_new_path_entry_both_or_None(
1685
# All changed entries need to have their parents be directories and be
1686
# at the right path. This set contains (path, id) tuples.
1688
# When we delete an item, all the children of it must be either deleted
1689
# or altered in their own right. As we batch process the change via
1690
# CHKMap.apply_delta, we build a set of things to use to validate the
1694
for old_path, new_path, file_id, entry in inventory_delta:
1697
result.root_id = file_id
1698
if new_path is None:
1703
if propagate_caches:
1705
del result._path_to_fileid_cache[old_path]
1708
deletes.add(file_id)
1710
new_key = StaticTuple(file_id,)
1711
new_value = result._entry_to_bytes(entry)
1712
# Update caches. It's worth doing this whether
1713
# we're propagating the old caches or not.
1714
result._path_to_fileid_cache[new_path] = file_id
1715
parents.add((split(new_path)[0], entry.parent_id))
1716
if old_path is None:
1719
old_key = StaticTuple(file_id,)
1720
if self.id2path(file_id) != old_path:
1721
raise errors.InconsistentDelta(old_path, file_id,
1722
"Entry was at wrong other path %r." %
1723
self.id2path(file_id))
1724
altered.add(file_id)
1725
id_to_entry_delta.append(StaticTuple(old_key, new_key, new_value))
1726
if result.parent_id_basename_to_file_id is not None:
1727
# parent_id, basename changes
1728
if old_path is None:
1731
old_entry = self.get_entry(file_id)
1732
old_key = self._parent_id_basename_key(old_entry)
1733
if new_path is None:
1737
new_key = self._parent_id_basename_key(entry)
1739
# If the two keys are the same, the value will be unchanged
1740
# as its always the file id for this entry.
1741
if old_key != new_key:
1742
# Transform a change into explicit delete/add preserving
1743
# a possible match on the key from a different file id.
1744
if old_key is not None:
1745
parent_id_basename_delta.setdefault(
1746
old_key, [None, None])[0] = old_key
1747
if new_key is not None:
1748
parent_id_basename_delta.setdefault(
1749
new_key, [None, None])[1] = new_value
1750
# validate that deletes are complete.
1751
for file_id in deletes:
1752
entry = self.get_entry(file_id)
1753
if entry.kind != 'directory':
1755
# This loop could potentially be better by using the id_basename
1756
# map to just get the child file ids.
1757
for child in viewvalues(entry.children):
1758
if child.file_id not in altered:
1759
raise errors.InconsistentDelta(self.id2path(child.file_id),
1760
child.file_id, "Child not deleted or reparented when "
1762
result.id_to_entry.apply_delta(id_to_entry_delta)
1763
if parent_id_basename_delta:
1764
# Transform the parent_id_basename delta data into a linear delta
1765
# with only one record for a given key. Optimally this would allow
1766
# re-keying, but its simpler to just output that as a delete+add
1767
# to spend less time calculating the delta.
1769
for key, (old_key, value) in viewitems(parent_id_basename_delta):
1770
if value is not None:
1771
delta_list.append((old_key, key, value))
1773
delta_list.append((old_key, None, None))
1774
result.parent_id_basename_to_file_id.apply_delta(delta_list)
1775
parents.discard(('', None))
1776
for parent_path, parent in parents:
1778
if result.get_entry(parent).kind != 'directory':
1779
raise errors.InconsistentDelta(result.id2path(parent), parent,
1780
'Not a directory, but given children')
1781
except errors.NoSuchId:
1782
raise errors.InconsistentDelta("<unknown>", parent,
1783
"Parent is not present in resulting inventory.")
1784
if result.path2id(parent_path) != parent:
1785
raise errors.InconsistentDelta(parent_path, parent,
1786
"Parent has wrong path %r." % result.path2id(parent_path))
1790
def deserialise(klass, chk_store, bytes, expected_revision_id):
1791
"""Deserialise a CHKInventory.
1793
:param chk_store: A CHK capable VersionedFiles instance.
1794
:param bytes: The serialised bytes.
1795
:param expected_revision_id: The revision ID we think this inventory is
1797
:return: A CHKInventory
1799
lines = bytes.split(b'\n')
1800
if lines[-1] != b'':
1801
raise AssertionError('bytes to deserialize must end with an eol')
1803
if lines[0] != b'chkinventory:':
1804
raise ValueError("not a serialised CHKInventory: %r" % bytes)
1806
allowed_keys = frozenset((b'root_id', b'revision_id',
1807
b'parent_id_basename_to_file_id',
1808
b'search_key_name', b'id_to_entry'))
1809
for line in lines[1:]:
1810
key, value = line.split(b': ', 1)
1811
if key not in allowed_keys:
1812
raise errors.BzrError('Unknown key in inventory: %r\n%r'
1815
raise errors.BzrError('Duplicate key in inventory: %r\n%r'
1818
revision_id = bytesintern(info[b'revision_id'])
1819
root_id = bytesintern(info[b'root_id'])
1820
search_key_name = bytesintern(info.get(b'search_key_name', b'plain'))
1821
parent_id_basename_to_file_id = bytesintern(info.get(
1822
b'parent_id_basename_to_file_id', None))
1823
if not parent_id_basename_to_file_id.startswith(b'sha1:'):
1824
raise ValueError('parent_id_basename_to_file_id should be a sha1'
1825
' key not %r' % (parent_id_basename_to_file_id,))
1826
id_to_entry = info[b'id_to_entry']
1827
if not id_to_entry.startswith(b'sha1:'):
1828
raise ValueError('id_to_entry should be a sha1'
1829
' key not %r' % (id_to_entry,))
1831
result = CHKInventory(search_key_name)
1832
result.revision_id = revision_id
1833
result.root_id = root_id
1834
search_key_func = chk_map.search_key_registry.get(
1835
result._search_key_name)
1836
if parent_id_basename_to_file_id is not None:
1837
result.parent_id_basename_to_file_id = chk_map.CHKMap(
1838
chk_store, StaticTuple(parent_id_basename_to_file_id,),
1839
search_key_func=search_key_func)
1841
result.parent_id_basename_to_file_id = None
1843
result.id_to_entry = chk_map.CHKMap(chk_store,
1844
StaticTuple(id_to_entry,),
1845
search_key_func=search_key_func)
1846
if (result.revision_id,) != expected_revision_id:
1847
raise ValueError("Mismatched revision id and expected: %r, %r" %
1848
(result.revision_id, expected_revision_id))
1852
def from_inventory(klass, chk_store, inventory, maximum_size=0, search_key_name=b'plain'):
1853
"""Create a CHKInventory from an existing inventory.
1855
The content of inventory is copied into the chk_store, and a
1856
CHKInventory referencing that is returned.
1858
:param chk_store: A CHK capable VersionedFiles instance.
1859
:param inventory: The inventory to copy.
1860
:param maximum_size: The CHKMap node size limit.
1861
:param search_key_name: The identifier for the search key function
1863
result = klass(search_key_name)
1864
result.revision_id = inventory.revision_id
1865
result.root_id = inventory.root.file_id
1867
entry_to_bytes = result._entry_to_bytes
1868
parent_id_basename_key = result._parent_id_basename_key
1869
id_to_entry_dict = {}
1870
parent_id_basename_dict = {}
1871
for path, entry in inventory.iter_entries():
1872
key = StaticTuple(entry.file_id,).intern()
1873
id_to_entry_dict[key] = entry_to_bytes(entry)
1874
p_id_key = parent_id_basename_key(entry)
1875
parent_id_basename_dict[p_id_key] = entry.file_id
1877
result._populate_from_dicts(chk_store, id_to_entry_dict,
1878
parent_id_basename_dict, maximum_size=maximum_size)
1881
def _populate_from_dicts(self, chk_store, id_to_entry_dict,
1882
parent_id_basename_dict, maximum_size):
1883
search_key_func = chk_map.search_key_registry.get(
1884
self._search_key_name)
1885
root_key = chk_map.CHKMap.from_dict(chk_store, id_to_entry_dict,
1886
maximum_size=maximum_size, key_width=1,
1887
search_key_func=search_key_func)
1888
self.id_to_entry = chk_map.CHKMap(chk_store, root_key,
1890
root_key = chk_map.CHKMap.from_dict(chk_store,
1891
parent_id_basename_dict,
1892
maximum_size=maximum_size, key_width=2,
1893
search_key_func=search_key_func)
1894
self.parent_id_basename_to_file_id = chk_map.CHKMap(chk_store,
1895
root_key, search_key_func)
1897
def _parent_id_basename_key(self, entry):
1898
"""Create a key for a entry in a parent_id_basename_to_file_id index."""
1899
if entry.parent_id is not None:
1900
parent_id = entry.parent_id
1903
return StaticTuple(parent_id, entry.name.encode('utf8')).intern()
1905
def get_entry(self, file_id):
1906
"""map a single file_id -> InventoryEntry."""
1908
raise errors.NoSuchId(self, file_id)
1909
result = self._fileid_to_entry_cache.get(file_id, None)
1910
if result is not None:
1913
return self._bytes_to_entry(
1914
next(self.id_to_entry.iteritems([StaticTuple(file_id,)]))[1])
1915
except StopIteration:
1916
# really we're passing an inventory, not a tree...
1917
raise errors.NoSuchId(self, file_id)
1919
def _getitems(self, file_ids):
1920
"""Similar to get_entry, but lets you query for multiple.
1922
The returned order is undefined. And currently if an item doesn't
1923
exist, it isn't included in the output.
1927
for file_id in file_ids:
1928
entry = self._fileid_to_entry_cache.get(file_id, None)
1930
remaining.append(file_id)
1932
result.append(entry)
1933
file_keys = [StaticTuple(f,).intern() for f in remaining]
1934
for file_key, value in self.id_to_entry.iteritems(file_keys):
1935
entry = self._bytes_to_entry(value)
1936
result.append(entry)
1937
self._fileid_to_entry_cache[entry.file_id] = entry
1940
def has_id(self, file_id):
1941
# Perhaps have an explicit 'contains' method on CHKMap ?
1942
if self._fileid_to_entry_cache.get(file_id, None) is not None:
1945
self.id_to_entry.iteritems([StaticTuple(file_id,)]))) == 1
1947
def is_root(self, file_id):
1948
return file_id == self.root_id
1950
def _iter_file_id_parents(self, file_id):
1951
"""Yield the parents of file_id up to the root."""
1952
while file_id is not None:
1954
ie = self.get_entry(file_id)
1956
raise errors.NoSuchId(tree=self, file_id=file_id)
1958
file_id = ie.parent_id
1960
def iter_all_ids(self):
1961
"""Iterate over all file-ids."""
1962
for key, _ in self.id_to_entry.iteritems():
1965
def iter_just_entries(self):
1966
"""Iterate over all entries.
1968
Unlike iter_entries(), just the entries are returned (not (path, ie))
1969
and the order of entries is undefined.
1971
XXX: We may not want to merge this into bzr.dev.
1973
for key, entry in self.id_to_entry.iteritems():
1975
ie = self._fileid_to_entry_cache.get(file_id, None)
1977
ie = self._bytes_to_entry(entry)
1978
self._fileid_to_entry_cache[file_id] = ie
1981
def _preload_cache(self):
1982
"""Make sure all file-ids are in _fileid_to_entry_cache"""
1983
if self._fully_cached:
1984
return # No need to do it again
1985
# The optimal sort order is to use iteritems() directly
1986
cache = self._fileid_to_entry_cache
1987
for key, entry in self.id_to_entry.iteritems():
1989
if file_id not in cache:
1990
ie = self._bytes_to_entry(entry)
1994
last_parent_id = last_parent_ie = None
1995
pid_items = self.parent_id_basename_to_file_id.iteritems()
1996
for key, child_file_id in pid_items:
1997
if key == (b'', b''): # This is the root
1998
if child_file_id != self.root_id:
1999
raise ValueError('Data inconsistency detected.'
2000
' We expected data with key ("","") to match'
2001
' the root id, but %s != %s'
2002
% (child_file_id, self.root_id))
2004
parent_id, basename = key
2005
ie = cache[child_file_id]
2006
if parent_id == last_parent_id:
2007
parent_ie = last_parent_ie
2009
parent_ie = cache[parent_id]
2010
if parent_ie.kind != 'directory':
2011
raise ValueError('Data inconsistency detected.'
2012
' An entry in the parent_id_basename_to_file_id map'
2013
' has parent_id {%s} but the kind of that object'
2014
' is %r not "directory"' % (parent_id, parent_ie.kind))
2015
if parent_ie._children is None:
2016
parent_ie._children = {}
2017
basename = basename.decode('utf-8')
2018
if basename in parent_ie._children:
2019
existing_ie = parent_ie._children[basename]
2020
if existing_ie != ie:
2021
raise ValueError('Data inconsistency detected.'
2022
' Two entries with basename %r were found'
2023
' in the parent entry {%s}'
2024
% (basename, parent_id))
2025
if basename != ie.name:
2026
raise ValueError('Data inconsistency detected.'
2027
' In the parent_id_basename_to_file_id map, file_id'
2028
' {%s} is listed as having basename %r, but in the'
2029
' id_to_entry map it is %r'
2030
% (child_file_id, basename, ie.name))
2031
parent_ie._children[basename] = ie
2032
self._fully_cached = True
2034
def iter_changes(self, basis):
2035
"""Generate a Tree.iter_changes change list between this and basis.
2037
:param basis: Another CHKInventory.
2038
:return: An iterator over the changes between self and basis, as per
2039
tree.iter_changes().
2041
# We want: (file_id, (path_in_source, path_in_target),
2042
# changed_content, versioned, parent, name, kind,
2044
for key, basis_value, self_value in \
2045
self.id_to_entry.iter_changes(basis.id_to_entry):
2047
if basis_value is not None:
2048
basis_entry = basis._bytes_to_entry(basis_value)
2049
path_in_source = basis.id2path(file_id)
2050
basis_parent = basis_entry.parent_id
2051
basis_name = basis_entry.name
2052
basis_executable = basis_entry.executable
2054
path_in_source = None
2057
basis_executable = None
2058
if self_value is not None:
2059
self_entry = self._bytes_to_entry(self_value)
2060
path_in_target = self.id2path(file_id)
2061
self_parent = self_entry.parent_id
2062
self_name = self_entry.name
2063
self_executable = self_entry.executable
2065
path_in_target = None
2068
self_executable = None
2069
if basis_value is None:
2071
kind = (None, self_entry.kind)
2072
versioned = (False, True)
2073
elif self_value is None:
2075
kind = (basis_entry.kind, None)
2076
versioned = (True, False)
2078
kind = (basis_entry.kind, self_entry.kind)
2079
versioned = (True, True)
2080
changed_content = False
2081
if kind[0] != kind[1]:
2082
changed_content = True
2083
elif kind[0] == 'file':
2084
if (self_entry.text_size != basis_entry.text_size
2085
or self_entry.text_sha1 != basis_entry.text_sha1):
2086
changed_content = True
2087
elif kind[0] == 'symlink':
2088
if self_entry.symlink_target != basis_entry.symlink_target:
2089
changed_content = True
2090
elif kind[0] == 'tree-reference':
2091
if (self_entry.reference_revision
2092
!= basis_entry.reference_revision):
2093
changed_content = True
2094
parent = (basis_parent, self_parent)
2095
name = (basis_name, self_name)
2096
executable = (basis_executable, self_executable)
2097
if (not changed_content and
2098
parent[0] == parent[1] and
2099
name[0] == name[1] and
2100
executable[0] == executable[1]):
2101
# Could happen when only the revision changed for a directory
2104
yield (file_id, (path_in_source, path_in_target), changed_content,
2105
versioned, parent, name, kind, executable)
2108
"""Return the number of entries in the inventory."""
2109
return len(self.id_to_entry)
2111
def _make_delta(self, old):
2112
"""Make an inventory delta from two inventories."""
2113
if not isinstance(old, CHKInventory):
2114
return CommonInventory._make_delta(self, old)
2116
for key, old_value, self_value in \
2117
self.id_to_entry.iter_changes(old.id_to_entry):
2119
if old_value is not None:
2120
old_path = old.id2path(file_id)
2123
if self_value is not None:
2124
entry = self._bytes_to_entry(self_value)
2125
self._fileid_to_entry_cache[file_id] = entry
2126
new_path = self.id2path(file_id)
2130
delta.append((old_path, new_path, file_id, entry))
2133
def path2id(self, relpath):
2134
"""See CommonInventory.path2id()."""
2135
# TODO: perhaps support negative hits?
2136
if isinstance(relpath, (str, text_type)):
2137
names = osutils.splitpath(relpath)
2142
relpath = osutils.pathjoin(*relpath)
2143
result = self._path_to_fileid_cache.get(relpath, None)
2144
if result is not None:
2146
current_id = self.root_id
2147
if current_id is None:
2149
parent_id_index = self.parent_id_basename_to_file_id
2151
for basename in names:
2152
if cur_path is None:
2155
cur_path = cur_path + '/' + basename
2156
basename_utf8 = basename.encode('utf8')
2157
file_id = self._path_to_fileid_cache.get(cur_path, None)
2159
key_filter = [StaticTuple(current_id, basename_utf8)]
2160
items = parent_id_index.iteritems(key_filter)
2161
for (parent_id, name_utf8), file_id in items:
2162
if parent_id != current_id or name_utf8 != basename_utf8:
2163
raise errors.BzrError("corrupt inventory lookup! "
2164
"%r %r %r %r" % (parent_id, current_id, name_utf8,
2169
self._path_to_fileid_cache[cur_path] = file_id
2170
current_id = file_id
2174
"""Serialise the inventory to lines."""
2175
lines = [b"chkinventory:\n"]
2176
if self._search_key_name != b'plain':
2177
# custom ordering grouping things that don't change together
2178
lines.append(b'search_key_name: %s\n' % (
2179
self._search_key_name))
2180
lines.append(b"root_id: %s\n" % self.root_id)
2181
lines.append(b'parent_id_basename_to_file_id: %s\n' %
2182
(self.parent_id_basename_to_file_id.key()[0],))
2183
lines.append(b"revision_id: %s\n" % self.revision_id)
2184
lines.append(b"id_to_entry: %s\n" % (self.id_to_entry.key()[0],))
2186
lines.append(b"revision_id: %s\n" % self.revision_id)
2187
lines.append(b"root_id: %s\n" % self.root_id)
2188
if self.parent_id_basename_to_file_id is not None:
2189
lines.append(b'parent_id_basename_to_file_id: %s\n' %
2190
(self.parent_id_basename_to_file_id.key()[0],))
2191
lines.append(b"id_to_entry: %s\n" % (self.id_to_entry.key()[0],))
2196
"""Get the root entry."""
2197
return self.get_entry(self.root_id)
2200
class CHKInventoryDirectory(InventoryDirectory):
2201
"""A directory in an inventory."""
2203
__slots__ = ['_children', '_chk_inventory']
2205
def __init__(self, file_id, name, parent_id, chk_inventory):
2206
# Don't call InventoryDirectory.__init__ - it isn't right for this
2208
InventoryEntry.__init__(self, file_id, name, parent_id)
2209
self._children = None
2210
self._chk_inventory = chk_inventory
2214
"""Access the list of children of this directory.
2216
With a parent_id_basename_to_file_id index, loads all the children,
2217
without loads the entire index. Without is bad. A more sophisticated
2218
proxy object might be nice, to allow partial loading of children as
2219
well when specific names are accessed. (So path traversal can be
2220
written in the obvious way but not examine siblings.).
2222
if self._children is not None:
2223
return self._children
2224
# No longer supported
2225
if self._chk_inventory.parent_id_basename_to_file_id is None:
2226
raise AssertionError("Inventories without"
2227
" parent_id_basename_to_file_id are no longer supported")
2229
# XXX: Todo - use proxy objects for the children rather than loading
2230
# all when the attribute is referenced.
2231
parent_id_index = self._chk_inventory.parent_id_basename_to_file_id
2233
for (parent_id, name_utf8), file_id in parent_id_index.iteritems(
2234
key_filter=[StaticTuple(self.file_id,)]):
2235
child_keys.add(StaticTuple(file_id,))
2237
for file_id_key in child_keys:
2238
entry = self._chk_inventory._fileid_to_entry_cache.get(
2239
file_id_key[0], None)
2240
if entry is not None:
2241
result[entry.name] = entry
2242
cached.add(file_id_key)
2243
child_keys.difference_update(cached)
2244
# populate; todo: do by name
2245
id_to_entry = self._chk_inventory.id_to_entry
2246
for file_id_key, bytes in id_to_entry.iteritems(child_keys):
2247
entry = self._chk_inventory._bytes_to_entry(bytes)
2248
result[entry.name] = entry
2249
self._chk_inventory._fileid_to_entry_cache[file_id_key[0]] = entry
2250
self._children = result
2255
'directory': InventoryDirectory,
2256
'file': InventoryFile,
2257
'symlink': InventoryLink,
2258
'tree-reference': TreeReference
2262
def make_entry(kind, name, parent_id, file_id=None):
2263
"""Create an inventory entry.
2265
:param kind: the type of inventory entry to create.
2266
:param name: the basename of the entry.
2267
:param parent_id: the parent_id of the entry.
2268
:param file_id: the file_id to use. if None, one will be created.
2271
file_id = generate_ids.gen_file_id(name)
2272
name = ensure_normalized_name(name)
2274
factory = entry_factory[kind]
2276
raise errors.BadFileKindError(name, kind)
2277
return factory(file_id, name, parent_id)
2280
def ensure_normalized_name(name):
2283
:raises InvalidNormalization: When name is not normalized, and cannot be
2284
accessed on this platform by the normalized path.
2285
:return: The NFC normalised version of name.
2287
# ------- This has been copied to breezy.dirstate.DirState.add, please
2288
# keep them synchronised.
2289
# we dont import normalized_filename directly because we want to be
2290
# able to change the implementation at runtime for tests.
2291
norm_name, can_access = osutils.normalized_filename(name)
2292
if norm_name != name:
2296
# TODO: jam 20060701 This would probably be more useful
2297
# if the error was raised with the full path
2298
raise errors.InvalidNormalization(name)
2302
_NAME_RE = lazy_regex.lazy_compile(r'^[^/\\]+$')
2305
def is_valid_name(name):
2306
return bool(_NAME_RE.match(name))
2309
def _check_delta_unique_ids(delta):
2310
"""Decorate a delta and check that the file ids in it are unique.
2312
:return: A generator over delta.
2316
length = len(ids) + 1
2318
if len(ids) != length:
2319
raise errors.InconsistentDelta(item[0] or item[1], item[2],
2324
def _check_delta_unique_new_paths(delta):
2325
"""Decorate a delta and check that the new paths in it are unique.
2327
:return: A generator over delta.
2331
length = len(paths) + 1
2333
if path is not None:
2335
if len(paths) != length:
2336
raise errors.InconsistentDelta(path, item[2], "repeated path")
2340
def _check_delta_unique_old_paths(delta):
2341
"""Decorate a delta and check that the old paths in it are unique.
2343
:return: A generator over delta.
2347
length = len(paths) + 1
2349
if path is not None:
2351
if len(paths) != length:
2352
raise errors.InconsistentDelta(path, item[2], "repeated path")
2356
def _check_delta_ids_are_valid(delta):
2357
"""Decorate a delta and check that the ids in it are valid.
2359
:return: A generator over delta.
2364
raise errors.InconsistentDelta(item[0] or item[1], item[2],
2365
"entry with file_id None %r" % entry)
2366
if not isinstance(item[2], bytes):
2367
raise errors.InconsistentDelta(item[0] or item[1], item[2],
2368
"entry with non bytes file_id %r" % entry)
2372
def _check_delta_ids_match_entry(delta):
2373
"""Decorate a delta and check that the ids in it match the entry.file_id.
2375
:return: A generator over delta.
2379
if entry is not None:
2380
if entry.file_id != item[2]:
2381
raise errors.InconsistentDelta(item[0] or item[1], item[2],
2382
"mismatched id with %r" % entry)
2386
def _check_delta_new_path_entry_both_or_None(delta):
2387
"""Decorate a delta and check that the new_path and entry are paired.
2389
:return: A generator over delta.
2394
if new_path is None and entry is not None:
2395
raise errors.InconsistentDelta(item[0], item[1],
2396
"Entry with no new_path")
2397
if new_path is not None and entry is None:
2398
raise errors.InconsistentDelta(new_path, item[1],
2399
"new_path with no entry")
2403
def mutable_inventory_from_tree(tree):
2404
"""Create a new inventory that has the same contents as a specified tree.
2406
:param tree: Revision tree to create inventory from
2408
entries = tree.iter_entries_by_dir()
2409
inv = Inventory(None, tree.get_revision_id())
2410
for path, inv_entry in entries:
2411
inv.add(inv_entry.copy())