~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory_delta.py

(spiv) Remove redundant parent inventories calculation during fetch. (Andrew
 Bennetts)

Show diffs side-by-side

added added

removed removed

Lines of Context:
29
29
from bzrlib import inventory
30
30
from bzrlib.revision import NULL_REVISION
31
31
 
 
32
FORMAT_1 = 'bzr inventory delta v1 (bzr 1.14)'
 
33
 
 
34
 
 
35
class InventoryDeltaError(errors.BzrError):
 
36
    """An error when serializing or deserializing an inventory delta."""
 
37
    
 
38
    # Most errors when serializing and deserializing are due to bugs, although
 
39
    # damaged input (i.e. a bug in a different process) could cause
 
40
    # deserialization errors too.
 
41
    internal_error = True
 
42
 
 
43
 
 
44
class IncompatibleInventoryDelta(errors.BzrError):
 
45
    """The delta could not be deserialised because its contents conflict with
 
46
    the allow_versioned_root or allow_tree_references flags of the
 
47
    deserializer.
 
48
    """
 
49
    internal_error = False
 
50
 
32
51
 
33
52
def _directory_content(entry):
34
53
    """Serialize the content component of entry which is a directory.
49
68
        exec_bytes = ''
50
69
    size_exec_sha = (entry.text_size, exec_bytes, entry.text_sha1)
51
70
    if None in size_exec_sha:
52
 
        raise errors.BzrError('Missing size or sha for %s' % entry.file_id)
 
71
        raise InventoryDeltaError('Missing size or sha for %s' % entry.file_id)
53
72
    return "file\x00%d\x00%s\x00%s" % size_exec_sha
54
73
 
55
74
 
60
79
    """
61
80
    target = entry.symlink_target
62
81
    if target is None:
63
 
        raise errors.BzrError('Missing target for %s' % entry.file_id)
 
82
        raise InventoryDeltaError('Missing target for %s' % entry.file_id)
64
83
    return "link\x00%s" % target.encode('utf8')
65
84
 
66
85
 
71
90
    """
72
91
    tree_revision = entry.reference_revision
73
92
    if tree_revision is None:
74
 
        raise errors.BzrError('Missing reference revision for %s' % entry.file_id)
 
93
        raise InventoryDeltaError(
 
94
            'Missing reference revision for %s' % entry.file_id)
75
95
    return "tree\x00%s" % tree_revision
76
96
 
77
97
 
115
135
    return result
116
136
 
117
137
 
118
 
 
119
138
class InventoryDeltaSerializer(object):
120
 
    """Serialize and deserialize inventory deltas."""
121
 
 
122
 
    FORMAT_1 = 'bzr inventory delta v1 (bzr 1.14)'
 
139
    """Serialize inventory deltas."""
123
140
 
124
141
    def __init__(self, versioned_root, tree_references):
125
142
        """Create an InventoryDeltaSerializer.
141
158
    def delta_to_lines(self, old_name, new_name, delta_to_new):
142
159
        """Return a line sequence for delta_to_new.
143
160
 
 
161
        Both the versioned_root and tree_references flags must be set via
 
162
        require_flags before calling this.
 
163
 
144
164
        :param old_name: A UTF8 revision id for the old inventory.  May be
145
165
            NULL_REVISION if there is no older inventory and delta_to_new
146
166
            includes the entire inventory contents.
150
170
            takes.
151
171
        :return: The serialized delta as lines.
152
172
        """
 
173
        if type(old_name) is not str:
 
174
            raise TypeError('old_name should be str, got %r' % (old_name,))
 
175
        if type(new_name) is not str:
 
176
            raise TypeError('new_name should be str, got %r' % (new_name,))
153
177
        lines = ['', '', '', '', '']
154
178
        to_line = self._delta_item_to_line
155
179
        for delta_item in delta_to_new:
156
 
            lines.append(to_line(delta_item))
157
 
            if lines[-1].__class__ != str:
158
 
                raise errors.BzrError(
 
180
            line = to_line(delta_item, new_name)
 
181
            if line.__class__ != str:
 
182
                raise InventoryDeltaError(
159
183
                    'to_line generated non-str output %r' % lines[-1])
 
184
            lines.append(line)
160
185
        lines.sort()
161
 
        lines[0] = "format: %s\n" % InventoryDeltaSerializer.FORMAT_1
 
186
        lines[0] = "format: %s\n" % FORMAT_1
162
187
        lines[1] = "parent: %s\n" % old_name
163
188
        lines[2] = "version: %s\n" % new_name
164
189
        lines[3] = "versioned_root: %s\n" % self._serialize_bool(
173
198
        else:
174
199
            return "false"
175
200
 
176
 
    def _delta_item_to_line(self, delta_item):
 
201
    def _delta_item_to_line(self, delta_item, new_version):
177
202
        """Convert delta_item to a line."""
178
203
        oldpath, newpath, file_id, entry = delta_item
179
204
        if newpath is None:
188
213
                oldpath_utf8 = 'None'
189
214
            else:
190
215
                oldpath_utf8 = '/' + oldpath.encode('utf8')
 
216
            if newpath == '/':
 
217
                raise AssertionError(
 
218
                    "Bad inventory delta: '/' is not a valid newpath "
 
219
                    "(should be '') in delta item %r" % (delta_item,))
191
220
            # TODO: Test real-world utf8 cache hit rate. It may be a win.
192
221
            newpath_utf8 = '/' + newpath.encode('utf8')
193
222
            # Serialize None as ''
196
225
            last_modified = entry.revision
197
226
            # special cases for /
198
227
            if newpath_utf8 == '/' and not self._versioned_root:
199
 
                if file_id != 'TREE_ROOT':
200
 
                    raise errors.BzrError(
201
 
                        'file_id %s is not TREE_ROOT for /' % file_id)
202
 
                if last_modified is not None:
203
 
                    raise errors.BzrError(
204
 
                        'Version present for / in %s' % file_id)
205
 
                last_modified = NULL_REVISION
 
228
                # This is an entry for the root, this inventory does not
 
229
                # support versioned roots.  So this must be an unversioned
 
230
                # root, i.e. last_modified == new revision.  Otherwise, this
 
231
                # delta is invalid.
 
232
                # Note: the non-rich-root repositories *can* have roots with
 
233
                # file-ids other than TREE_ROOT, e.g. repo formats that use the
 
234
                # xml5 serializer.
 
235
                if last_modified != new_version:
 
236
                    raise InventoryDeltaError(
 
237
                        'Version present for / in %s (%s != %s)'
 
238
                        % (file_id, last_modified, new_version))
206
239
            if last_modified is None:
207
 
                raise errors.BzrError("no version for fileid %s" % file_id)
 
240
                raise InventoryDeltaError("no version for fileid %s" % file_id)
208
241
            content = self._entry_to_content[entry.kind](entry)
209
242
        return ("%s\x00%s\x00%s\x00%s\x00%s\x00%s\n" %
210
243
            (oldpath_utf8, newpath_utf8, file_id, parent_id, last_modified,
211
244
                content))
212
245
 
 
246
 
 
247
class InventoryDeltaDeserializer(object):
 
248
    """Deserialize inventory deltas."""
 
249
 
 
250
    def __init__(self, allow_versioned_root=True, allow_tree_references=True):
 
251
        """Create an InventoryDeltaDeserializer.
 
252
 
 
253
        :param versioned_root: If True, any root entry that is seen is expected
 
254
            to be versioned, and root entries can have any fileid.
 
255
        :param tree_references: If True support tree-reference entries.
 
256
        """
 
257
        self._allow_versioned_root = allow_versioned_root
 
258
        self._allow_tree_references = allow_tree_references
 
259
 
213
260
    def _deserialize_bool(self, value):
214
261
        if value == "true":
215
262
            return True
216
263
        elif value == "false":
217
264
            return False
218
265
        else:
219
 
            raise errors.BzrError("value %r is not a bool" % (value,))
 
266
            raise InventoryDeltaError("value %r is not a bool" % (value,))
220
267
 
221
268
    def parse_text_bytes(self, bytes):
222
269
        """Parse the text bytes of a serialized inventory delta.
223
270
 
 
271
        If versioned_root and/or tree_references flags were set via
 
272
        require_flags, then the parsed flags must match or a BzrError will be
 
273
        raised.
 
274
 
224
275
        :param bytes: The bytes to parse. This can be obtained by calling
225
276
            delta_to_lines and then doing ''.join(delta_lines).
226
 
        :return: (parent_id, new_id, inventory_delta)
 
277
        :return: (parent_id, new_id, versioned_root, tree_references,
 
278
            inventory_delta)
227
279
        """
 
280
        if bytes[-1:] != '\n':
 
281
            last_line = bytes.rsplit('\n', 1)[-1]
 
282
            raise InventoryDeltaError('last line not empty: %r' % (last_line,))
228
283
        lines = bytes.split('\n')[:-1] # discard the last empty line
229
 
        if not lines or lines[0] != 'format: %s' % InventoryDeltaSerializer.FORMAT_1:
230
 
            raise errors.BzrError('unknown format %r' % lines[0:1])
 
284
        if not lines or lines[0] != 'format: %s' % FORMAT_1:
 
285
            raise InventoryDeltaError('unknown format %r' % lines[0:1])
231
286
        if len(lines) < 2 or not lines[1].startswith('parent: '):
232
 
            raise errors.BzrError('missing parent: marker')
 
287
            raise InventoryDeltaError('missing parent: marker')
233
288
        delta_parent_id = lines[1][8:]
234
289
        if len(lines) < 3 or not lines[2].startswith('version: '):
235
 
            raise errors.BzrError('missing version: marker')
 
290
            raise InventoryDeltaError('missing version: marker')
236
291
        delta_version_id = lines[2][9:]
237
292
        if len(lines) < 4 or not lines[3].startswith('versioned_root: '):
238
 
            raise errors.BzrError('missing versioned_root: marker')
 
293
            raise InventoryDeltaError('missing versioned_root: marker')
239
294
        delta_versioned_root = self._deserialize_bool(lines[3][16:])
240
295
        if len(lines) < 5 or not lines[4].startswith('tree_references: '):
241
 
            raise errors.BzrError('missing tree_references: marker')
 
296
            raise InventoryDeltaError('missing tree_references: marker')
242
297
        delta_tree_references = self._deserialize_bool(lines[4][17:])
243
 
        if delta_versioned_root != self._versioned_root:
244
 
            raise errors.BzrError(
245
 
                "serialized versioned_root flag is wrong: %s" %
246
 
                (delta_versioned_root,))
247
 
        if delta_tree_references != self._tree_references:
248
 
            raise errors.BzrError(
249
 
                "serialized tree_references flag is wrong: %s" %
250
 
                (delta_tree_references,))
 
298
        if (not self._allow_versioned_root and delta_versioned_root):
 
299
            raise IncompatibleInventoryDelta("versioned_root not allowed")
251
300
        result = []
252
301
        seen_ids = set()
253
302
        line_iter = iter(lines)
258
307
                content) = line.split('\x00', 5)
259
308
            parent_id = parent_id or None
260
309
            if file_id in seen_ids:
261
 
                raise errors.BzrError(
 
310
                raise InventoryDeltaError(
262
311
                    "duplicate file id in inventory delta %r" % lines)
263
312
            seen_ids.add(file_id)
264
 
            if newpath_utf8 == '/' and not delta_versioned_root and (
265
 
                last_modified != 'null:' or file_id != 'TREE_ROOT'):
266
 
                    raise errors.BzrError("Versioned root found: %r" % line)
267
 
            elif last_modified[-1] == ':':
268
 
                    raise errors.BzrError('special revisionid found: %r' % line)
269
 
            if not delta_tree_references and content.startswith('tree\x00'):
270
 
                raise errors.BzrError("Tree reference found: %r" % line)
271
 
            content_tuple = tuple(content.split('\x00'))
272
 
            entry = _parse_entry(
273
 
                newpath_utf8, file_id, parent_id, last_modified, content_tuple)
 
313
            if (newpath_utf8 == '/' and not delta_versioned_root and
 
314
                last_modified != delta_version_id):
 
315
                    # Delta claims to be not have a versioned root, yet here's
 
316
                    # a root entry with a non-default version.
 
317
                    raise InventoryDeltaError("Versioned root found: %r" % line)
 
318
            elif newpath_utf8 != 'None' and last_modified[-1] == ':':
 
319
                # Deletes have a last_modified of null:, but otherwise special
 
320
                # revision ids should not occur.
 
321
                raise InventoryDeltaError('special revisionid found: %r' % line)
 
322
            if content.startswith('tree\x00'):
 
323
                if delta_tree_references is False:
 
324
                    raise InventoryDeltaError(
 
325
                            "Tree reference found (but header said "
 
326
                            "tree_references: false): %r" % line)
 
327
                elif not self._allow_tree_references:
 
328
                    raise IncompatibleInventoryDelta(
 
329
                        "Tree reference not allowed")
274
330
            if oldpath_utf8 == 'None':
275
331
                oldpath = None
 
332
            elif oldpath_utf8[:1] != '/':
 
333
                raise InventoryDeltaError(
 
334
                    "oldpath invalid (does not start with /): %r"
 
335
                    % (oldpath_utf8,))
276
336
            else:
 
337
                oldpath_utf8 = oldpath_utf8[1:]
277
338
                oldpath = oldpath_utf8.decode('utf8')
278
339
            if newpath_utf8 == 'None':
279
340
                newpath = None
 
341
            elif newpath_utf8[:1] != '/':
 
342
                raise InventoryDeltaError(
 
343
                    "newpath invalid (does not start with /): %r"
 
344
                    % (newpath_utf8,))
280
345
            else:
 
346
                # Trim leading slash
 
347
                newpath_utf8 = newpath_utf8[1:]
281
348
                newpath = newpath_utf8.decode('utf8')
 
349
            content_tuple = tuple(content.split('\x00'))
 
350
            if content_tuple[0] == 'deleted':
 
351
                entry = None
 
352
            else:
 
353
                entry = _parse_entry(
 
354
                    newpath, file_id, parent_id, last_modified, content_tuple)
282
355
            delta_item = (oldpath, newpath, file_id, entry)
283
356
            result.append(delta_item)
284
 
        return delta_parent_id, delta_version_id, result
285
 
 
286
 
 
287
 
def _parse_entry(utf8_path, file_id, parent_id, last_modified, content):
 
357
        return (delta_parent_id, delta_version_id, delta_versioned_root,
 
358
                delta_tree_references, result)
 
359
 
 
360
 
 
361
def _parse_entry(path, file_id, parent_id, last_modified, content):
288
362
    entry_factory = {
289
363
        'dir': _dir_to_entry,
290
364
        'file': _file_to_entry,
292
366
        'tree': _tree_to_entry,
293
367
    }
294
368
    kind = content[0]
295
 
    path = utf8_path[1:].decode('utf8')
 
369
    if path.startswith('/'):
 
370
        raise AssertionError
296
371
    name = basename(path)
297
372
    return entry_factory[content[0]](
298
373
            content, name, parent_id, file_id, last_modified)
299
374
 
 
375