128
130
RENAMED = 'renamed'
129
131
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
135
def detect_changes(self, old_entry):
150
136
"""Return a (text_modified, meta_modified) from this to old_entry.
229
231
known_kinds = ('file', 'directory', 'symlink')
233
def _put_in_tar(self, item, tree):
234
"""populate item for stashing in a tar, and return the content stream.
236
If no content is available, return None.
238
raise BzrError("don't know how to export {%s} of kind %r" %
239
(self.file_id, self.kind))
241
@deprecated_method(deprecated_in((1, 6, 0)))
242
def put_on_disk(self, dest, dp, tree):
243
"""Create a representation of self on disk in the prefix dest.
245
This is a template method - implement _put_on_disk in subclasses.
247
fullpath = osutils.pathjoin(dest, dp)
248
self._put_on_disk(fullpath, tree)
249
# mutter(" export {%s} kind %s to %s", self.file_id,
250
# self.kind, fullpath)
252
def _put_on_disk(self, fullpath, tree):
253
"""Put this entry onto disk at fullpath, from tree tree."""
254
raise BzrError("don't know how to export {%s} of kind %r" % (self.file_id, self.kind))
231
256
def sorted_children(self):
232
257
return sorted(self.children.items())
247
272
:param rev_id: Revision id from which this InventoryEntry was loaded.
248
273
Not necessarily the last-changed revision for this file.
249
274
:param inv: Inventory from which the entry was loaded.
275
:param tree: RevisionTree for this entry.
251
277
if self.parent_id is not None:
252
278
if not inv.has_id(self.parent_id):
253
279
raise BzrCheckError('missing parent {%s} in inventory for revision {%s}'
254
280
% (self.parent_id, rev_id))
255
checker._add_entry_to_text_key_references(inv, self)
256
self._check(checker, rev_id)
281
self._check(checker, rev_id, tree)
258
def _check(self, checker, rev_id):
283
def _check(self, checker, rev_id, tree):
259
284
"""Check this inventory entry for kind specific errors."""
260
checker._report_items.append(
261
'unknown entry kind %r in revision {%s}' % (self.kind, rev_id))
285
raise BzrCheckError('unknown entry kind %r in revision {%s}' %
264
289
"""Clone this inventory entry."""
399
class RootEntry(InventoryEntry):
401
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
402
'text_id', 'parent_id', 'children', 'executable',
403
'revision', 'symlink_target', 'reference_revision']
405
def _check(self, checker, rev_id, tree):
406
"""See InventoryEntry._check"""
408
def __init__(self, file_id):
409
self.file_id = file_id
411
self.kind = 'directory'
412
self.parent_id = None
415
symbol_versioning.warn('RootEntry is deprecated as of bzr 0.10.'
416
' Please use InventoryDirectory instead.',
417
DeprecationWarning, stacklevel=2)
419
def __eq__(self, other):
420
if not isinstance(other, RootEntry):
421
return NotImplemented
423
return (self.file_id == other.file_id) \
424
and (self.children == other.children)
374
427
class InventoryDirectory(InventoryEntry):
375
428
"""A directory in an inventory."""
377
__slots__ = ['children']
381
def _check(self, checker, rev_id):
430
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
431
'text_id', 'parent_id', 'children', 'executable',
432
'revision', 'symlink_target', 'reference_revision']
434
def _check(self, checker, rev_id, tree):
382
435
"""See InventoryEntry._check"""
383
# In non rich root repositories we do not expect a file graph for the
385
if self.name == '' and not checker.rich_roots:
387
# Directories are stored as an empty file, but the file should exist
388
# to provide a per-fileid log. The hash of every directory content is
389
# "da..." below (the sha1sum of '').
390
checker.add_pending_item(rev_id,
391
('texts', self.file_id, self.revision), 'text',
392
'da39a3ee5e6b4b0d3255bfef95601890afd80709')
436
if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
437
raise BzrCheckError('directory {%s} has text in revision {%s}'
438
% (self.file_id, rev_id))
395
441
other = InventoryDirectory(self.file_id, self.name, self.parent_id)
401
447
def __init__(self, file_id, name, parent_id):
402
448
super(InventoryDirectory, self).__init__(file_id, name, parent_id)
403
449
self.children = {}
450
self.kind = 'directory'
405
452
def kind_character(self):
406
453
"""See InventoryEntry.kind_character."""
456
def _put_in_tar(self, item, tree):
457
"""See InventoryEntry._put_in_tar."""
458
item.type = tarfile.DIRTYPE
465
def _put_on_disk(self, fullpath, tree):
466
"""See InventoryEntry._put_on_disk."""
410
470
class InventoryFile(InventoryEntry):
411
471
"""A file in an inventory."""
413
__slots__ = ['text_sha1', 'text_size', 'text_id', 'executable']
417
def __init__(self, file_id, name, parent_id):
418
super(InventoryFile, self).__init__(file_id, name, parent_id)
419
self.text_sha1 = None
420
self.text_size = None
422
self.executable = False
424
def _check(self, checker, tree_revision_id):
473
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
474
'text_id', 'parent_id', 'children', 'executable',
475
'revision', 'symlink_target', 'reference_revision']
477
def _check(self, checker, tree_revision_id, tree):
425
478
"""See InventoryEntry._check"""
426
# TODO: check size too.
427
checker.add_pending_item(tree_revision_id,
428
('texts', self.file_id, self.revision), 'text',
430
if self.text_size is None:
431
checker._report_items.append(
432
'fileid {%s} in {%s} has None for text_size' % (self.file_id,
479
key = (self.file_id, self.revision)
480
if key in checker.checked_texts:
481
prev_sha = checker.checked_texts[key]
482
if prev_sha != self.text_sha1:
484
'mismatched sha1 on {%s} in {%s} (%s != %s) %r' %
485
(self.file_id, tree_revision_id, prev_sha, self.text_sha1,
488
checker.repeated_text_cnt += 1
491
checker.checked_text_cnt += 1
492
# We can't check the length, because Weave doesn't store that
493
# information, and the whole point of looking at the weave's
494
# sha1sum is that we don't have to extract the text.
495
if (self.text_sha1 != tree._repository.texts.get_sha1s([key])[key]):
496
raise BzrCheckError('text {%s} version {%s} wrong sha1' % key)
497
checker.checked_texts[key] = self.text_sha1
436
500
other = InventoryFile(self.file_id, self.name, self.parent_id)
468
532
"""See InventoryEntry.has_text."""
535
def __init__(self, file_id, name, parent_id):
536
super(InventoryFile, self).__init__(file_id, name, parent_id)
471
539
def kind_character(self):
472
540
"""See InventoryEntry.kind_character."""
543
def _put_in_tar(self, item, tree):
544
"""See InventoryEntry._put_in_tar."""
545
item.type = tarfile.REGTYPE
546
fileobj = tree.get_file(self.file_id)
547
item.size = self.text_size
548
if tree.is_executable(self.file_id):
554
def _put_on_disk(self, fullpath, tree):
555
"""See InventoryEntry._put_on_disk."""
556
osutils.pumpfile(tree.get_file(self.file_id), file(fullpath, 'wb'))
557
if tree.is_executable(self.file_id):
558
os.chmod(fullpath, 0755)
475
560
def _read_tree_state(self, path, work_tree):
476
561
"""See InventoryEntry._read_tree_state."""
477
562
self.text_sha1 = work_tree.get_file_sha1(self.file_id, path=path)
509
594
class InventoryLink(InventoryEntry):
510
595
"""A file in an inventory."""
512
__slots__ = ['symlink_target']
516
def __init__(self, file_id, name, parent_id):
517
super(InventoryLink, self).__init__(file_id, name, parent_id)
518
self.symlink_target = None
520
def _check(self, checker, tree_revision_id):
597
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
598
'text_id', 'parent_id', 'children', 'executable',
599
'revision', 'symlink_target', 'reference_revision']
601
def _check(self, checker, rev_id, tree):
521
602
"""See InventoryEntry._check"""
603
if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
604
raise BzrCheckError('symlink {%s} has text in revision {%s}'
605
% (self.file_id, rev_id))
522
606
if self.symlink_target is None:
523
checker._report_items.append(
524
'symlink {%s} has no target in revision {%s}'
525
% (self.file_id, tree_revision_id))
526
# Symlinks are stored as ''
527
checker.add_pending_item(tree_revision_id,
528
('texts', self.file_id, self.revision), 'text',
529
'da39a3ee5e6b4b0d3255bfef95601890afd80709')
607
raise BzrCheckError('symlink {%s} has no target in revision {%s}'
608
% (self.file_id, rev_id))
532
611
other = InventoryLink(self.file_id, self.name, self.parent_id)
562
641
differ = DiffSymlink(old_tree, new_tree, output_to)
563
642
return differ.diff_symlink(old_target, new_target)
644
def __init__(self, file_id, name, parent_id):
645
super(InventoryLink, self).__init__(file_id, name, parent_id)
646
self.kind = 'symlink'
565
648
def kind_character(self):
566
649
"""See InventoryEntry.kind_character."""
652
def _put_in_tar(self, item, tree):
653
"""See InventoryEntry._put_in_tar."""
654
item.type = tarfile.SYMTYPE
658
item.linkname = self.symlink_target
661
def _put_on_disk(self, fullpath, tree):
662
"""See InventoryEntry._put_on_disk."""
664
os.symlink(self.symlink_target, fullpath)
666
raise BzrError("Failed to create symlink %r -> %r, error: %s" % (fullpath, self.symlink_target, e))
569
668
def _read_tree_state(self, path, work_tree):
570
669
"""See InventoryEntry._read_tree_state."""
571
670
self.symlink_target = work_tree.get_symlink_target(self.file_id)
617
714
class CommonInventory(object):
618
"""Basic inventory logic, defined in terms of primitives like has_id.
620
An inventory is the metadata about the contents of a tree.
622
This is broadly a map from file_id to entries such as directories, files,
623
symlinks and tree references. Each entry maintains its own metadata like
624
SHA1 and length for files, or children for a directory.
626
Entries can be looked up either by path or by file_id.
628
InventoryEntry objects must not be modified after they are
629
inserted, other than through the Inventory API.
715
"""Basic inventory logic, defined in terms of primitives like has_id."""
632
717
def __contains__(self, file_id):
633
718
"""True if this entry contains a file with given id.
856
935
descend(self.root, u'')
859
def path2id(self, relpath):
938
def path2id(self, name):
860
939
"""Walk down through directories to return entry of last component.
862
:param relpath: may be either a list of path components, or a single
863
string, in which case it is automatically split.
941
names may be either a list of path components, or a single
942
string, in which case it is automatically split.
865
944
This returns the entry of the last component in the path,
866
945
which may be either a file or a directory.
868
947
Returns None IFF the path is not found.
870
if isinstance(relpath, basestring):
871
names = osutils.splitpath(relpath)
949
if isinstance(name, basestring):
950
name = osutils.splitpath(name)
952
# mutter("lookup path %r" % name)
876
955
parent = self.root
942
1021
class Inventory(CommonInventory):
943
"""Mutable dict based in-memory inventory.
945
We never store the full path to a file, because renaming a directory
946
implicitly moves all of its contents. This class internally maintains a
1022
"""Inventory of versioned files in a tree.
1024
This describes which file_id is present at each point in the tree,
1025
and possibly the SHA-1 or other information about the file.
1026
Entries can be looked up either by path or by file_id.
1028
The inventory represents a typical unix file tree, with
1029
directories containing files and subdirectories. We never store
1030
the full path to a file, because renaming a directory implicitly
1031
moves all of its contents. This class internally maintains a
947
1032
lookup tree that allows the children under a directory to be
948
1033
returned quickly.
1035
InventoryEntry objects must not be modified after they are
1036
inserted, other than through the Inventory API.
950
1038
>>> inv = Inventory()
951
1039
>>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
952
1040
InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
953
1041
>>> inv['123-123'].name
956
Id's may be looked up from paths:
1044
May be treated as an iterator or set to look up file ids:
958
>>> inv.path2id('hello.c')
1046
>>> bool(inv.path2id('hello.c'))
960
1048
>>> '123-123' in inv
963
There are iterators over the contents:
1051
May also look up by name:
965
>>> [entry[0] for entry in inv.iter_entries()]
1053
>>> [x[0] for x in inv.iter_entries()]
966
1054
['', u'hello.c']
1055
>>> inv = Inventory('TREE_ROOT-12345678-12345678')
1056
>>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
1057
Traceback (most recent call last):
1058
BzrError: parent_id {TREE_ROOT} not in inventory
1059
>>> inv.add(InventoryFile('123-123', 'hello.c', 'TREE_ROOT-12345678-12345678'))
1060
InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678', sha1=None, len=None, revision=None)
969
1062
def __init__(self, root_id=ROOT_ID, revision_id=None):
970
1063
"""Create or read an inventory.
1052
1141
# modified children remaining by the time we examine it.
1053
1142
for old_path, file_id in sorted(((op, f) for op, np, f, e in delta
1054
1143
if op is not None), reverse=True):
1144
if file_id not in self:
1055
1147
# Preserve unaltered children of file_id for later reinsertion.
1056
1148
file_id_children = getattr(self[file_id], 'children', {})
1057
1149
if len(file_id_children):
1058
1150
children[file_id] = file_id_children
1059
if self.id2path(file_id) != old_path:
1060
raise errors.InconsistentDelta(old_path, file_id,
1061
"Entry was at wrong other path %r." % self.id2path(file_id))
1062
1151
# Remove file_id and the unaltered children. If file_id is not
1063
1152
# being deleted it will be reinserted back later.
1064
1153
self.remove_recursive_id(file_id)
1078
1167
replacement.revision = new_entry.revision
1079
1168
replacement.children = children.pop(replacement.file_id, {})
1080
1169
new_entry = replacement
1083
except errors.DuplicateFileId:
1084
raise errors.InconsistentDelta(new_path, new_entry.file_id,
1085
"New id is already present in target.")
1086
except AttributeError:
1087
raise errors.InconsistentDelta(new_path, new_entry.file_id,
1088
"Parent is not a directory.")
1089
if self.id2path(new_entry.file_id) != new_path:
1090
raise errors.InconsistentDelta(new_path, new_entry.file_id,
1091
"New path is not consistent with parent path.")
1092
1171
if len(children):
1093
1172
# Get the parent id that was deleted
1094
1173
parent_id, children = children.popitem()
1095
1174
raise errors.InconsistentDelta("<deleted>", parent_id,
1096
1175
"The file id was deleted but its children were not deleted.")
1098
def create_by_apply_delta(self, inventory_delta, new_revision_id,
1099
propagate_caches=False):
1100
"""See CHKInventory.create_by_apply_delta()"""
1101
new_inv = self.copy()
1102
new_inv.apply_delta(inventory_delta)
1103
new_inv.revision_id = new_revision_id
1106
1177
def _set_root(self, ie):
1108
1179
self._byid = {self.root.file_id: self.root}
1179
1250
def add(self, entry):
1180
1251
"""Add entry to inventory.
1253
To add a file to a branch ready to be committed, use Branch.add,
1256
Returns the new entry object.
1184
1258
if entry.file_id in self._byid:
1185
1259
raise errors.DuplicateFileId(entry.file_id,
1186
1260
self._byid[entry.file_id])
1187
1262
if entry.parent_id is None:
1188
1263
self.root = entry
1191
1266
parent = self._byid[entry.parent_id]
1192
1267
except KeyError:
1193
raise errors.InconsistentDelta("<unknown>", entry.parent_id,
1194
"Parent not in inventory.")
1268
raise BzrError("parent_id {%s} not in inventory" %
1195
1271
if entry.name in parent.children:
1196
raise errors.InconsistentDelta(
1197
self.id2path(parent.children[entry.name].file_id),
1199
"Path already versioned")
1272
raise BzrError("%s is already versioned" %
1273
osutils.pathjoin(self.id2path(parent.file_id),
1274
entry.name).encode('utf-8'))
1200
1275
parent.children[entry.name] = entry
1201
1276
return self._add_child(entry)
1450
1527
raise ValueError("unknown kind %r" % entry.kind)
1452
def _expand_fileids_to_parents_and_children(self, file_ids):
1453
"""Give a more wholistic view starting with the given file_ids.
1455
For any file_id which maps to a directory, we will include all children
1456
of that directory. We will also include all directories which are
1457
parents of the given file_ids, but we will not include their children.
1464
fringle # fringle-id
1468
if given [foo-id] we will include
1469
TREE_ROOT as interesting parents
1471
foo-id, baz-id, frob-id, fringle-id
1475
# TODO: Pre-pass over the list of fileids to see if anything is already
1476
# deserialized in self._fileid_to_entry_cache
1478
directories_to_expand = set()
1479
children_of_parent_id = {}
1480
# It is okay if some of the fileids are missing
1481
for entry in self._getitems(file_ids):
1482
if entry.kind == 'directory':
1483
directories_to_expand.add(entry.file_id)
1484
interesting.add(entry.parent_id)
1485
children_of_parent_id.setdefault(entry.parent_id, []
1486
).append(entry.file_id)
1488
# Now, interesting has all of the direct parents, but not the
1489
# parents of those parents. It also may have some duplicates with
1491
remaining_parents = interesting.difference(file_ids)
1492
# When we hit the TREE_ROOT, we'll get an interesting parent of None,
1493
# but we don't actually want to recurse into that
1494
interesting.add(None) # this will auto-filter it in the loop
1495
remaining_parents.discard(None)
1496
while remaining_parents:
1497
next_parents = set()
1498
for entry in self._getitems(remaining_parents):
1499
next_parents.add(entry.parent_id)
1500
children_of_parent_id.setdefault(entry.parent_id, []
1501
).append(entry.file_id)
1502
# Remove any search tips we've already processed
1503
remaining_parents = next_parents.difference(interesting)
1504
interesting.update(remaining_parents)
1505
# We should probably also .difference(directories_to_expand)
1506
interesting.update(file_ids)
1507
interesting.discard(None)
1508
while directories_to_expand:
1509
# Expand directories by looking in the
1510
# parent_id_basename_to_file_id map
1511
keys = [StaticTuple(f,).intern() for f in directories_to_expand]
1512
directories_to_expand = set()
1513
items = self.parent_id_basename_to_file_id.iteritems(keys)
1514
next_file_ids = set([item[1] for item in items])
1515
next_file_ids = next_file_ids.difference(interesting)
1516
interesting.update(next_file_ids)
1517
for entry in self._getitems(next_file_ids):
1518
if entry.kind == 'directory':
1519
directories_to_expand.add(entry.file_id)
1520
children_of_parent_id.setdefault(entry.parent_id, []
1521
).append(entry.file_id)
1522
return interesting, children_of_parent_id
1524
def filter(self, specific_fileids):
1525
"""Get an inventory view filtered against a set of file-ids.
1527
Children of directories and parents are included.
1529
The result may or may not reference the underlying inventory
1530
so it should be treated as immutable.
1533
parent_to_children) = self._expand_fileids_to_parents_and_children(
1535
# There is some overlap here, but we assume that all interesting items
1536
# are in the _fileid_to_entry_cache because we had to read them to
1537
# determine if they were a dir we wanted to recurse, or just a file
1538
# This should give us all the entries we'll want to add, so start
1540
other = Inventory(self.root_id)
1541
other.root.revision = self.root.revision
1542
other.revision_id = self.revision_id
1543
if not interesting or not parent_to_children:
1544
# empty filter, or filtering entrys that don't exist
1545
# (if even 1 existed, then we would have populated
1546
# parent_to_children with at least the tree root.)
1548
cache = self._fileid_to_entry_cache
1549
remaining_children = collections.deque(parent_to_children[self.root_id])
1550
while remaining_children:
1551
file_id = remaining_children.popleft()
1553
if ie.kind == 'directory':
1554
ie = ie.copy() # We create a copy to depopulate the .children attribute
1555
# TODO: depending on the uses of 'other' we should probably alwyas
1556
# '.copy()' to prevent someone from mutating other and
1557
# invaliding our internal cache
1559
if file_id in parent_to_children:
1560
remaining_children.extend(parent_to_children[file_id])
1564
1530
def _bytes_to_utf8name_key(bytes):
1565
1531
"""Get the file_id, revision_id key out of bytes."""
1639
1603
search_key_func=search_key_func)
1640
1604
result.id_to_entry._ensure_root()
1641
1605
result.id_to_entry._root_node.set_maximum_size(maximum_size)
1642
# Change to apply to the parent_id_basename delta. The dict maps
1643
# (parent_id, basename) -> (old_key, new_value). We use a dict because
1644
# when a path has its id replaced (e.g. the root is changed, or someone
1645
# does bzr mv a b, bzr mv c a, we should output a single change to this
1646
# map rather than two.
1647
parent_id_basename_delta = {}
1606
parent_id_basename_delta = []
1648
1607
if self.parent_id_basename_to_file_id is not None:
1649
1608
result.parent_id_basename_to_file_id = chk_map.CHKMap(
1650
1609
self.parent_id_basename_to_file_id._store,
1660
1619
result.parent_id_basename_to_file_id = None
1661
1620
result.root_id = self.root_id
1662
1621
id_to_entry_delta = []
1663
# inventory_delta is only traversed once, so we just update the
1665
# Check for repeated file ids
1666
inventory_delta = _check_delta_unique_ids(inventory_delta)
1667
# Repeated old paths
1668
inventory_delta = _check_delta_unique_old_paths(inventory_delta)
1669
# Check for repeated new paths
1670
inventory_delta = _check_delta_unique_new_paths(inventory_delta)
1671
# Check for entries that don't match the fileid
1672
inventory_delta = _check_delta_ids_match_entry(inventory_delta)
1673
# Check for nonsense fileids
1674
inventory_delta = _check_delta_ids_are_valid(inventory_delta)
1675
# Check for new_path <-> entry consistency
1676
inventory_delta = _check_delta_new_path_entry_both_or_None(
1678
# All changed entries need to have their parents be directories and be
1679
# at the right path. This set contains (path, id) tuples.
1681
# When we delete an item, all the children of it must be either deleted
1682
# or altered in their own right. As we batch process the change via
1683
# CHKMap.apply_delta, we build a set of things to use to validate the
1687
1622
for old_path, new_path, file_id, entry in inventory_delta:
1688
1623
# file id changes
1689
1624
if new_path == '':
1698
1633
del result._path_to_fileid_cache[old_path]
1699
1634
except KeyError:
1701
deletes.add(file_id)
1703
new_key = StaticTuple(file_id,)
1637
new_key = (file_id,)
1704
1638
new_value = result._entry_to_bytes(entry)
1705
1639
# Update caches. It's worth doing this whether
1706
1640
# we're propagating the old caches or not.
1707
1641
result._path_to_fileid_cache[new_path] = file_id
1708
parents.add((split(new_path)[0], entry.parent_id))
1709
1642
if old_path is None:
1712
old_key = StaticTuple(file_id,)
1713
if self.id2path(file_id) != old_path:
1714
raise errors.InconsistentDelta(old_path, file_id,
1715
"Entry was at wrong other path %r." %
1716
self.id2path(file_id))
1717
altered.add(file_id)
1718
id_to_entry_delta.append(StaticTuple(old_key, new_key, new_value))
1645
old_key = (file_id,)
1646
id_to_entry_delta.append((old_key, new_key, new_value))
1719
1647
if result.parent_id_basename_to_file_id is not None:
1720
1648
# parent_id, basename changes
1721
1649
if old_path is None:
1730
1658
new_key = self._parent_id_basename_key(entry)
1731
1659
new_value = file_id
1732
# If the two keys are the same, the value will be unchanged
1733
# as its always the file id for this entry.
1734
1660
if old_key != new_key:
1735
# Transform a change into explicit delete/add preserving
1736
# a possible match on the key from a different file id.
1737
if old_key is not None:
1738
parent_id_basename_delta.setdefault(
1739
old_key, [None, None])[0] = old_key
1740
if new_key is not None:
1741
parent_id_basename_delta.setdefault(
1742
new_key, [None, None])[1] = new_value
1743
# validate that deletes are complete.
1744
for file_id in deletes:
1745
entry = self[file_id]
1746
if entry.kind != 'directory':
1748
# This loop could potentially be better by using the id_basename
1749
# map to just get the child file ids.
1750
for child in entry.children.values():
1751
if child.file_id not in altered:
1752
raise errors.InconsistentDelta(self.id2path(child.file_id),
1753
child.file_id, "Child not deleted or reparented when "
1661
# If the two keys are the same, the value will be unchanged
1662
# as its always the file id.
1663
parent_id_basename_delta.append((old_key, new_key, new_value))
1755
1664
result.id_to_entry.apply_delta(id_to_entry_delta)
1756
1665
if parent_id_basename_delta:
1757
# Transform the parent_id_basename delta data into a linear delta
1758
# with only one record for a given key. Optimally this would allow
1759
# re-keying, but its simpler to just output that as a delete+add
1760
# to spend less time calculating the delta.
1762
for key, (old_key, value) in parent_id_basename_delta.iteritems():
1763
if value is not None:
1764
delta_list.append((old_key, key, value))
1766
delta_list.append((old_key, None, None))
1767
result.parent_id_basename_to_file_id.apply_delta(delta_list)
1768
parents.discard(('', None))
1769
for parent_path, parent in parents:
1771
if result[parent].kind != 'directory':
1772
raise errors.InconsistentDelta(result.id2path(parent), parent,
1773
'Not a directory, but given children')
1774
except errors.NoSuchId:
1775
raise errors.InconsistentDelta("<unknown>", parent,
1776
"Parent is not present in resulting inventory.")
1777
if result.path2id(parent_path) != parent:
1778
raise errors.InconsistentDelta(parent_path, parent,
1779
"Parent has wrong path %r." % result.path2id(parent_path))
1666
result.parent_id_basename_to_file_id.apply_delta(parent_id_basename_delta)
1808
1695
raise errors.BzrError('Duplicate key in inventory: %r\n%r'
1809
1696
% (key, bytes))
1810
1697
info[key] = value
1811
revision_id = intern(info['revision_id'])
1812
root_id = intern(info['root_id'])
1813
search_key_name = intern(info.get('search_key_name', 'plain'))
1814
parent_id_basename_to_file_id = intern(info.get(
1815
'parent_id_basename_to_file_id', None))
1816
if not parent_id_basename_to_file_id.startswith('sha1:'):
1817
raise ValueError('parent_id_basename_to_file_id should be a sha1'
1818
' key not %r' % (parent_id_basename_to_file_id,))
1698
revision_id = info['revision_id']
1699
root_id = info['root_id']
1700
search_key_name = info.get('search_key_name', 'plain')
1701
parent_id_basename_to_file_id = info.get(
1702
'parent_id_basename_to_file_id', None)
1819
1703
id_to_entry = info['id_to_entry']
1820
if not id_to_entry.startswith('sha1:'):
1821
raise ValueError('id_to_entry should be a sha1'
1822
' key not %r' % (id_to_entry,))
1824
1705
result = CHKInventory(search_key_name)
1825
1706
result.revision_id = revision_id
1828
1709
result._search_key_name)
1829
1710
if parent_id_basename_to_file_id is not None:
1830
1711
result.parent_id_basename_to_file_id = chk_map.CHKMap(
1831
chk_store, StaticTuple(parent_id_basename_to_file_id,),
1712
chk_store, (parent_id_basename_to_file_id,),
1832
1713
search_key_func=search_key_func)
1834
1715
result.parent_id_basename_to_file_id = None
1836
result.id_to_entry = chk_map.CHKMap(chk_store,
1837
StaticTuple(id_to_entry,),
1717
result.id_to_entry = chk_map.CHKMap(chk_store, (id_to_entry,),
1838
1718
search_key_func=search_key_func)
1839
1719
if (result.revision_id,) != expected_revision_id:
1840
1720
raise ValueError("Mismatched revision id and expected: %r, %r" %
1905
1784
return self._bytes_to_entry(
1906
self.id_to_entry.iteritems([StaticTuple(file_id,)]).next()[1])
1785
self.id_to_entry.iteritems([(file_id,)]).next()[1])
1907
1786
except StopIteration:
1908
1787
# really we're passing an inventory, not a tree...
1909
1788
raise errors.NoSuchId(self, file_id)
1911
def _getitems(self, file_ids):
1912
"""Similar to __getitem__, but lets you query for multiple.
1914
The returned order is undefined. And currently if an item doesn't
1915
exist, it isn't included in the output.
1919
for file_id in file_ids:
1920
entry = self._fileid_to_entry_cache.get(file_id, None)
1922
remaining.append(file_id)
1924
result.append(entry)
1925
file_keys = [StaticTuple(f,).intern() for f in remaining]
1926
for file_key, value in self.id_to_entry.iteritems(file_keys):
1927
entry = self._bytes_to_entry(value)
1928
result.append(entry)
1929
self._fileid_to_entry_cache[entry.file_id] = entry
1932
1790
def has_id(self, file_id):
1933
1791
# Perhaps have an explicit 'contains' method on CHKMap ?
1934
1792
if self._fileid_to_entry_cache.get(file_id, None) is not None:
1937
self.id_to_entry.iteritems([StaticTuple(file_id,)]))) == 1
1794
return len(list(self.id_to_entry.iteritems([(file_id,)]))) == 1
1939
1796
def is_root(self, file_id):
1940
1797
return file_id == self.root_id
2069
1926
delta.append((old_path, new_path, file_id, entry))
2072
def path2id(self, relpath):
1929
def path2id(self, name):
2073
1930
"""See CommonInventory.path2id()."""
2074
# TODO: perhaps support negative hits?
2075
result = self._path_to_fileid_cache.get(relpath, None)
2076
if result is not None:
2078
if isinstance(relpath, basestring):
2079
names = osutils.splitpath(relpath)
2082
current_id = self.root_id
2083
if current_id is None:
2085
parent_id_index = self.parent_id_basename_to_file_id
2087
for basename in names:
2088
if cur_path is None:
2091
cur_path = cur_path + '/' + basename
2092
basename_utf8 = basename.encode('utf8')
2093
file_id = self._path_to_fileid_cache.get(cur_path, None)
2095
key_filter = [StaticTuple(current_id, basename_utf8)]
2096
items = parent_id_index.iteritems(key_filter)
2097
for (parent_id, name_utf8), file_id in items:
2098
if parent_id != current_id or name_utf8 != basename_utf8:
2099
raise errors.BzrError("corrupt inventory lookup! "
2100
"%r %r %r %r" % (parent_id, current_id, name_utf8,
2105
self._path_to_fileid_cache[cur_path] = file_id
2106
current_id = file_id
1931
result = self._path_to_fileid_cache.get(name, None)
1933
result = CommonInventory.path2id(self, name)
1934
self._path_to_fileid_cache[name] = result
2109
1937
def to_lines(self):
2110
1938
"""Serialise the inventory to lines."""
2114
1942
lines.append('search_key_name: %s\n' % (self._search_key_name,))
2115
1943
lines.append("root_id: %s\n" % self.root_id)
2116
1944
lines.append('parent_id_basename_to_file_id: %s\n' %
2117
(self.parent_id_basename_to_file_id.key()[0],))
1945
self.parent_id_basename_to_file_id.key())
2118
1946
lines.append("revision_id: %s\n" % self.revision_id)
2119
lines.append("id_to_entry: %s\n" % (self.id_to_entry.key()[0],))
1947
lines.append("id_to_entry: %s\n" % self.id_to_entry.key())
2121
1949
lines.append("revision_id: %s\n" % self.revision_id)
2122
1950
lines.append("root_id: %s\n" % self.root_id)
2123
1951
if self.parent_id_basename_to_file_id is not None:
2124
1952
lines.append('parent_id_basename_to_file_id: %s\n' %
2125
(self.parent_id_basename_to_file_id.key()[0],))
2126
lines.append("id_to_entry: %s\n" % (self.id_to_entry.key()[0],))
1953
self.parent_id_basename_to_file_id.key())
1954
lines.append("id_to_entry: %s\n" % self.id_to_entry.key())
2135
1963
class CHKInventoryDirectory(InventoryDirectory):
2136
1964
"""A directory in an inventory."""
2138
__slots__ = ['_children', '_chk_inventory']
1966
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
1967
'text_id', 'parent_id', '_children', 'executable',
1968
'revision', 'symlink_target', 'reference_revision',
2140
1971
def __init__(self, file_id, name, parent_id, chk_inventory):
2141
1972
# Don't call InventoryDirectory.__init__ - it isn't right for this
2143
1974
InventoryEntry.__init__(self, file_id, name, parent_id)
2144
1975
self._children = None
1976
self.kind = 'directory'
2145
1977
self._chk_inventory = chk_inventory
2240
2072
_NAME_RE = re.compile(r'^[^/\\]+$')
2242
2074
return bool(_NAME_RE.match(name))
2245
def _check_delta_unique_ids(delta):
2246
"""Decorate a delta and check that the file ids in it are unique.
2248
:return: A generator over delta.
2252
length = len(ids) + 1
2254
if len(ids) != length:
2255
raise errors.InconsistentDelta(item[0] or item[1], item[2],
2260
def _check_delta_unique_new_paths(delta):
2261
"""Decorate a delta and check that the new paths in it are unique.
2263
:return: A generator over delta.
2267
length = len(paths) + 1
2269
if path is not None:
2271
if len(paths) != length:
2272
raise errors.InconsistentDelta(path, item[2], "repeated path")
2276
def _check_delta_unique_old_paths(delta):
2277
"""Decorate a delta and check that the old paths in it are unique.
2279
:return: A generator over delta.
2283
length = len(paths) + 1
2285
if path is not None:
2287
if len(paths) != length:
2288
raise errors.InconsistentDelta(path, item[2], "repeated path")
2292
def _check_delta_ids_are_valid(delta):
2293
"""Decorate a delta and check that the ids in it are valid.
2295
:return: A generator over delta.
2300
raise errors.InconsistentDelta(item[0] or item[1], item[2],
2301
"entry with file_id None %r" % entry)
2302
if type(item[2]) != str:
2303
raise errors.InconsistentDelta(item[0] or item[1], item[2],
2304
"entry with non bytes file_id %r" % entry)
2308
def _check_delta_ids_match_entry(delta):
2309
"""Decorate a delta and check that the ids in it match the entry.file_id.
2311
:return: A generator over delta.
2315
if entry is not None:
2316
if entry.file_id != item[2]:
2317
raise errors.InconsistentDelta(item[0] or item[1], item[2],
2318
"mismatched id with %r" % entry)
2322
def _check_delta_new_path_entry_both_or_None(delta):
2323
"""Decorate a delta and check that the new_path and entry are paired.
2325
:return: A generator over delta.
2330
if new_path is None and entry is not None:
2331
raise errors.InconsistentDelta(item[0], item[1],
2332
"Entry with no new_path")
2333
if new_path is not None and entry is None:
2334
raise errors.InconsistentDelta(new_path, item[1],
2335
"new_path with no entry")