716
714
class CommonInventory(object):
717
"""Basic inventory logic, defined in terms of primitives like has_id."""
715
"""Basic inventory logic, defined in terms of primitives like has_id.
717
An inventory is the metadata about the contents of a tree.
719
This is broadly a map from file_id to entries such as directories, files,
720
symlinks and tree references. Each entry maintains its own metadata like
721
SHA1 and length for files, or children for a directory.
723
Entries can be looked up either by path or by file_id.
725
InventoryEntry objects must not be modified after they are
726
inserted, other than through the Inventory API.
719
729
def __contains__(self, file_id):
720
730
"""True if this entry contains a file with given id.
1023
1033
class Inventory(CommonInventory):
1024
"""Inventory of versioned files in a tree.
1026
This describes which file_id is present at each point in the tree,
1027
and possibly the SHA-1 or other information about the file.
1028
Entries can be looked up either by path or by file_id.
1030
The inventory represents a typical unix file tree, with
1031
directories containing files and subdirectories. We never store
1032
the full path to a file, because renaming a directory implicitly
1033
moves all of its contents. This class internally maintains a
1034
"""Mutable dict based in-memory inventory.
1036
We never store the full path to a file, because renaming a directory
1037
implicitly moves all of its contents. This class internally maintains a
1034
1038
lookup tree that allows the children under a directory to be
1035
1039
returned quickly.
1037
InventoryEntry objects must not be modified after they are
1038
inserted, other than through the Inventory API.
1040
1041
>>> inv = Inventory()
1041
1042
>>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
1042
1043
InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
1043
1044
>>> inv['123-123'].name
1046
May be treated as an iterator or set to look up file ids:
1047
Id's may be looked up from paths:
1048
>>> bool(inv.path2id('hello.c'))
1049
>>> inv.path2id('hello.c')
1050
1051
>>> '123-123' in inv
1053
May also look up by name:
1054
There are iterators over the contents:
1055
>>> [x[0] for x in inv.iter_entries()]
1056
>>> [entry[0] for entry in inv.iter_entries()]
1056
1057
['', u'hello.c']
1057
>>> inv = Inventory('TREE_ROOT-12345678-12345678')
1058
>>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
1059
Traceback (most recent call last):
1060
BzrError: parent_id {TREE_ROOT} not in inventory
1061
>>> inv.add(InventoryFile('123-123', 'hello.c', 'TREE_ROOT-12345678-12345678'))
1062
InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678', sha1=None, len=None, revision=None)
1064
1060
def __init__(self, root_id=ROOT_ID, revision_id=None):
1065
1061
"""Create or read an inventory.
1090
1086
def apply_delta(self, delta):
1091
1087
"""Apply a delta to this inventory.
1089
See the inventory developers documentation for the theory behind
1093
1092
:param delta: A list of changes to apply. After all the changes are
1094
1093
applied the final inventory must be internally consistent, but it
1095
1094
is ok to supply changes which, if only half-applied would have an
1127
1126
# Check that the delta is legal. It would be nice if this could be
1128
1127
# done within the loops below but it's safer to validate the delta
1129
# before starting to mutate the inventory.
1130
unique_file_ids = set([f for _, _, f, _ in delta])
1131
if len(unique_file_ids) != len(delta):
1132
raise AssertionError("a file-id appears multiple times in %r"
1128
# before starting to mutate the inventory, as there isn't a rollback
1130
list(_check_delta_unique_ids(_check_delta_unique_new_paths(
1131
_check_delta_unique_old_paths(_check_delta_ids_match_entry(
1137
1135
# Remove all affected items which were in the original inventory,
1166
1164
replacement.revision = new_entry.revision
1167
1165
replacement.children = children.pop(replacement.file_id, {})
1168
1166
new_entry = replacement
1169
except AttributeError:
1170
raise errors.InconsistentDelta(new_path, new_entry.file_id,
1171
"Parent is not a directory.")
1170
1172
if len(children):
1171
1173
# Get the parent id that was deleted
1172
1174
parent_id, children = children.popitem()
1265
1267
parent = self._byid[entry.parent_id]
1266
1268
except KeyError:
1267
raise BzrError("parent_id {%s} not in inventory" %
1269
raise errors.InconsistentDelta("<unknown>", entry.parent_id,
1270
"Parent not in inventory.")
1270
1271
if entry.name in parent.children:
1271
raise BzrError("%s is already versioned" %
1272
osutils.pathjoin(self.id2path(parent.file_id),
1273
entry.name).encode('utf-8'))
1272
raise errors.InconsistentDelta(
1273
self.id2path(parent.children[entry.name].file_id),
1275
"Path already versioned")
1274
1276
parent.children[entry.name] = entry
1275
1277
return self._add_child(entry)
1615
1620
result.parent_id_basename_to_file_id = None
1616
1621
result.root_id = self.root_id
1617
1622
id_to_entry_delta = []
1623
# inventory_delta is only traversed once, so we just update the
1625
# Check for repeated file ids
1626
inventory_delta = _check_delta_unique_ids(inventory_delta)
1627
# Repeated old paths
1628
inventory_delta = _check_delta_unique_old_paths(inventory_delta)
1629
# Check for repeated new paths
1630
inventory_delta = _check_delta_unique_new_paths(inventory_delta)
1631
# Check for entries that don't match the fileid
1632
inventory_delta = _check_delta_ids_match_entry(inventory_delta)
1633
# All changed entries need to have their parents be directories.
1618
1635
for old_path, new_path, file_id, entry in inventory_delta:
1619
1636
# file id changes
1620
1637
if new_path == '':
1660
1678
result.id_to_entry.apply_delta(id_to_entry_delta)
1661
1679
if parent_id_basename_delta:
1662
1680
result.parent_id_basename_to_file_id.apply_delta(parent_id_basename_delta)
1681
parents.discard(None)
1682
for parent in parents:
1684
if result[parent].kind != 'directory':
1685
raise errors.InconsistentDelta(result.id2path(parent), parent,
1686
'Not a directory, but given children')
1687
except errors.NoSuchId:
1688
raise errors.InconsistentDelta("<unknown>", parent,
1689
"Parent is not present in resulting inventory.")
2068
2095
_NAME_RE = re.compile(r'^[^/\\]+$')
2070
2097
return bool(_NAME_RE.match(name))
2100
def _check_delta_unique_ids(delta):
2101
"""Decorate a delta and check that the file ids in it are unique.
2103
:return: A generator over delta.
2107
length = len(ids) + 1
2109
if len(ids) != length:
2110
raise errors.InconsistentDelta(item[0] or item[1], item[2],
2115
def _check_delta_unique_new_paths(delta):
2116
"""Decorate a delta and check that the new paths in it are unique.
2118
:return: A generator over delta.
2122
length = len(paths) + 1
2124
if path is not None:
2126
if len(paths) != length:
2127
raise errors.InconsistentDelta(path, item[2], "repeated path")
2131
def _check_delta_unique_old_paths(delta):
2132
"""Decorate a delta and check that the old paths in it are unique.
2134
:return: A generator over delta.
2138
length = len(paths) + 1
2140
if path is not None:
2142
if len(paths) != length:
2143
raise errors.InconsistentDelta(path, item[2], "repeated path")
2147
def _check_delta_ids_match_entry(delta):
2148
"""Decorate a delta and check that the ids in it match the entry.file_id.
2150
:return: A generator over delta.
2154
if entry is not None:
2155
if entry.file_id != item[2]:
2156
raise errors.InconsistentDelta(item[0] or item[1], item[2],
2157
"mismatched id with %r" % entry)