~bzr-pqm/bzr/1.17

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2009-07-10 06:35:38 UTC
  • mfrom: (4505.5.9 apply-inventory-delta)
  • Revision ID: pqm@pqm.ubuntu.com-20090710063538-2hap9pxafqfe6r20
(robertc) First tranche of inventory-delta application validation.
        (Robert Collins)

Show diffs side-by-side

added added

removed removed

Lines of Context:
712
712
 
713
713
 
714
714
class CommonInventory(object):
715
 
    """Basic inventory logic, defined in terms of primitives like has_id."""
 
715
    """Basic inventory logic, defined in terms of primitives like has_id.
 
716
 
 
717
    An inventory is the metadata about the contents of a tree.
 
718
 
 
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.
 
722
 
 
723
    Entries can be looked up either by path or by file_id.
 
724
 
 
725
    InventoryEntry objects must not be modified after they are
 
726
    inserted, other than through the Inventory API.
 
727
    """
716
728
 
717
729
    def __contains__(self, file_id):
718
730
        """True if this entry contains a file with given id.
1019
1031
 
1020
1032
 
1021
1033
class Inventory(CommonInventory):
1022
 
    """Inventory of versioned files in a tree.
1023
 
 
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.
1027
 
 
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
 
1034
    """Mutable dict based in-memory inventory.
 
1035
 
 
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
1032
1038
    lookup tree that allows the children under a directory to be
1033
1039
    returned quickly.
1034
1040
 
1035
 
    InventoryEntry objects must not be modified after they are
1036
 
    inserted, other than through the Inventory API.
1037
 
 
1038
1041
    >>> inv = Inventory()
1039
1042
    >>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
1040
1043
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None, revision=None)
1041
1044
    >>> inv['123-123'].name
1042
1045
    'hello.c'
1043
1046
 
1044
 
    May be treated as an iterator or set to look up file ids:
 
1047
    Id's may be looked up from paths:
1045
1048
 
1046
 
    >>> bool(inv.path2id('hello.c'))
1047
 
    True
 
1049
    >>> inv.path2id('hello.c')
 
1050
    '123-123'
1048
1051
    >>> '123-123' in inv
1049
1052
    True
1050
1053
 
1051
 
    May also look up by name:
 
1054
    There are iterators over the contents:
1052
1055
 
1053
 
    >>> [x[0] for x in inv.iter_entries()]
 
1056
    >>> [entry[0] for entry in inv.iter_entries()]
1054
1057
    ['', 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)
1061
1058
    """
 
1059
 
1062
1060
    def __init__(self, root_id=ROOT_ID, revision_id=None):
1063
1061
        """Create or read an inventory.
1064
1062
 
1127
1125
        """
1128
1126
        # Check that the delta is legal. It would be nice if this could be
1129
1127
        # done within the loops below but it's safer to validate the delta
1130
 
        # before starting to mutate the inventory.
1131
 
        unique_file_ids = set([f for _, _, f, _ in delta])
1132
 
        if len(unique_file_ids) != len(delta):
1133
 
            raise AssertionError("a file-id appears multiple times in %r"
1134
 
                    % (delta,))
1135
 
        del unique_file_ids
 
1128
        # before starting to mutate the inventory, as there isn't a rollback
 
1129
        # facility.
 
1130
        list(_check_delta_unique_ids(_check_delta_unique_new_paths(
 
1131
            _check_delta_unique_old_paths(_check_delta_ids_match_entry(
 
1132
            delta)))))
1136
1133
 
1137
1134
        children = {}
1138
1135
        # Remove all affected items which were in the original inventory,
1167
1164
                replacement.revision = new_entry.revision
1168
1165
                replacement.children = children.pop(replacement.file_id, {})
1169
1166
                new_entry = replacement
1170
 
            self.add(new_entry)
 
1167
            try:
 
1168
                self.add(new_entry)
 
1169
            except AttributeError:
 
1170
                raise errors.InconsistentDelta(new_path, new_entry.file_id,
 
1171
                    "Parent is not a directory.")
1171
1172
        if len(children):
1172
1173
            # Get the parent id that was deleted
1173
1174
            parent_id, children = children.popitem()
1265
1266
            try:
1266
1267
                parent = self._byid[entry.parent_id]
1267
1268
            except KeyError:
1268
 
                raise BzrError("parent_id {%s} not in inventory" %
1269
 
                               entry.parent_id)
1270
 
 
 
1269
                raise errors.InconsistentDelta("<unknown>", entry.parent_id,
 
1270
                    "Parent not in inventory.")
1271
1271
            if entry.name in parent.children:
1272
 
                raise BzrError("%s is already versioned" %
1273
 
                        osutils.pathjoin(self.id2path(parent.file_id),
1274
 
                        entry.name).encode('utf-8'))
 
1272
                raise errors.InconsistentDelta(
 
1273
                    self.id2path(parent.children[entry.name].file_id),
 
1274
                    entry.file_id,
 
1275
                    "Path already versioned")
1275
1276
            parent.children[entry.name] = entry
1276
1277
        return self._add_child(entry)
1277
1278
 
1619
1620
            result.parent_id_basename_to_file_id = None
1620
1621
        result.root_id = self.root_id
1621
1622
        id_to_entry_delta = []
 
1623
        # inventory_delta is only traversed once, so we just update the
 
1624
        # variable.
 
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.
 
1634
        parents = set()
1622
1635
        for old_path, new_path, file_id, entry in inventory_delta:
1623
1636
            # file id changes
1624
1637
            if new_path == '':
1639
1652
                # Update caches. It's worth doing this whether
1640
1653
                # we're propagating the old caches or not.
1641
1654
                result._path_to_fileid_cache[new_path] = file_id
 
1655
                parents.add(entry.parent_id)
1642
1656
            if old_path is None:
1643
1657
                old_key = None
1644
1658
            else:
1664
1678
        result.id_to_entry.apply_delta(id_to_entry_delta)
1665
1679
        if parent_id_basename_delta:
1666
1680
            result.parent_id_basename_to_file_id.apply_delta(parent_id_basename_delta)
 
1681
        parents.discard(None)
 
1682
        for parent in parents:
 
1683
            try:
 
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.")
1667
1690
        return result
1668
1691
 
1669
1692
    @classmethod
2072
2095
        _NAME_RE = re.compile(r'^[^/\\]+$')
2073
2096
 
2074
2097
    return bool(_NAME_RE.match(name))
 
2098
 
 
2099
 
 
2100
def _check_delta_unique_ids(delta):
 
2101
    """Decorate a delta and check that the file ids in it are unique.
 
2102
 
 
2103
    :return: A generator over delta.
 
2104
    """
 
2105
    ids = set()
 
2106
    for item in delta:
 
2107
        length = len(ids) + 1
 
2108
        ids.add(item[2])
 
2109
        if len(ids) != length:
 
2110
            raise errors.InconsistentDelta(item[0] or item[1], item[2],
 
2111
                "repeated file_id")
 
2112
        yield item
 
2113
 
 
2114
 
 
2115
def _check_delta_unique_new_paths(delta):
 
2116
    """Decorate a delta and check that the new paths in it are unique.
 
2117
 
 
2118
    :return: A generator over delta.
 
2119
    """
 
2120
    paths = set()
 
2121
    for item in delta:
 
2122
        length = len(paths) + 1
 
2123
        path = item[1]
 
2124
        if path is not None:
 
2125
            paths.add(path)
 
2126
            if len(paths) != length:
 
2127
                raise errors.InconsistentDelta(path, item[2], "repeated path")
 
2128
        yield item
 
2129
 
 
2130
 
 
2131
def _check_delta_unique_old_paths(delta):
 
2132
    """Decorate a delta and check that the old paths in it are unique.
 
2133
 
 
2134
    :return: A generator over delta.
 
2135
    """
 
2136
    paths = set()
 
2137
    for item in delta:
 
2138
        length = len(paths) + 1
 
2139
        path = item[0]
 
2140
        if path is not None:
 
2141
            paths.add(path)
 
2142
            if len(paths) != length:
 
2143
                raise errors.InconsistentDelta(path, item[2], "repeated path")
 
2144
        yield item
 
2145
 
 
2146
 
 
2147
def _check_delta_ids_match_entry(delta):
 
2148
    """Decorate a delta and check that the ids in it match the entry.file_id.
 
2149
 
 
2150
    :return: A generator over delta.
 
2151
    """
 
2152
    for item in delta:
 
2153
        entry = item[3]
 
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)
 
2158
        yield item