/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/bzr/inventory_delta.py

[merge] robertc's integration, updated tests to check for retcode=3

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2008, 2009 Canonical Ltd
2
 
#
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.
7
 
#
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.
12
 
#
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
16
 
 
17
 
"""Inventory delta serialisation.
18
 
 
19
 
See doc/developers/inventory.txt for the description of the format.
20
 
 
21
 
In this module the interesting classes are:
22
 
 - InventoryDeltaSerializer - object to read/write inventory deltas.
23
 
"""
24
 
 
25
 
from __future__ import absolute_import
26
 
 
27
 
__all__ = ['InventoryDeltaSerializer']
28
 
 
29
 
from .. import errors
30
 
from ..osutils import basename
31
 
from . import inventory
32
 
from ..revision import NULL_REVISION
33
 
 
34
 
FORMAT_1 = b'bzr inventory delta v1 (bzr 1.14)'
35
 
 
36
 
 
37
 
class InventoryDeltaError(errors.BzrError):
38
 
    """An error when serializing or deserializing an inventory delta."""
39
 
 
40
 
    # Most errors when serializing and deserializing are due to bugs, although
41
 
    # damaged input (i.e. a bug in a different process) could cause
42
 
    # deserialization errors too.
43
 
    internal_error = True
44
 
 
45
 
    def __init__(self, format_string, **kwargs):
46
 
        # Let each error supply a custom format string and arguments.
47
 
        self._fmt = format_string
48
 
        super(InventoryDeltaError, self).__init__(**kwargs)
49
 
 
50
 
 
51
 
class IncompatibleInventoryDelta(errors.BzrError):
52
 
    """The delta could not be deserialised because its contents conflict with
53
 
    the allow_versioned_root or allow_tree_references flags of the
54
 
    deserializer.
55
 
    """
56
 
    internal_error = False
57
 
 
58
 
 
59
 
def _directory_content(entry):
60
 
    """Serialize the content component of entry which is a directory.
61
 
 
62
 
    :param entry: An InventoryDirectory.
63
 
    """
64
 
    return b"dir"
65
 
 
66
 
 
67
 
def _file_content(entry):
68
 
    """Serialize the content component of entry which is a file.
69
 
 
70
 
    :param entry: An InventoryFile.
71
 
    """
72
 
    if entry.executable:
73
 
        exec_bytes = b'Y'
74
 
    else:
75
 
        exec_bytes = b''
76
 
    size_exec_sha = entry.text_size, exec_bytes, entry.text_sha1
77
 
    if None in size_exec_sha:
78
 
        raise InventoryDeltaError(
79
 
            'Missing size or sha for %(fileid)r', fileid=entry.file_id)
80
 
    return b"file\x00%d\x00%s\x00%s" % size_exec_sha
81
 
 
82
 
 
83
 
def _link_content(entry):
84
 
    """Serialize the content component of entry which is a symlink.
85
 
 
86
 
    :param entry: An InventoryLink.
87
 
    """
88
 
    target = entry.symlink_target
89
 
    if target is None:
90
 
        raise InventoryDeltaError(
91
 
            'Missing target for %(fileid)r', fileid=entry.file_id)
92
 
    return b"link\x00%s" % target.encode('utf8')
93
 
 
94
 
 
95
 
def _reference_content(entry):
96
 
    """Serialize the content component of entry which is a tree-reference.
97
 
 
98
 
    :param entry: A TreeReference.
99
 
    """
100
 
    tree_revision = entry.reference_revision
101
 
    if tree_revision is None:
102
 
        raise InventoryDeltaError(
103
 
            'Missing reference revision for %(fileid)r', fileid=entry.file_id)
104
 
    return b"tree\x00%s" % tree_revision
105
 
 
106
 
 
107
 
def _dir_to_entry(content, name, parent_id, file_id, last_modified,
108
 
                  _type=inventory.InventoryDirectory):
109
 
    """Convert a dir content record to an InventoryDirectory."""
110
 
    result = _type(file_id, name, parent_id)
111
 
    result.revision = last_modified
112
 
    return result
113
 
 
114
 
 
115
 
def _file_to_entry(content, name, parent_id, file_id, last_modified,
116
 
                   _type=inventory.InventoryFile):
117
 
    """Convert a dir content record to an InventoryFile."""
118
 
    result = _type(file_id, name, parent_id)
119
 
    result.revision = last_modified
120
 
    result.text_size = int(content[1])
121
 
    result.text_sha1 = content[3]
122
 
    if content[2]:
123
 
        result.executable = True
124
 
    else:
125
 
        result.executable = False
126
 
    return result
127
 
 
128
 
 
129
 
def _link_to_entry(content, name, parent_id, file_id, last_modified,
130
 
                   _type=inventory.InventoryLink):
131
 
    """Convert a link content record to an InventoryLink."""
132
 
    result = _type(file_id, name, parent_id)
133
 
    result.revision = last_modified
134
 
    result.symlink_target = content[1].decode('utf8')
135
 
    return result
136
 
 
137
 
 
138
 
def _tree_to_entry(content, name, parent_id, file_id, last_modified,
139
 
                   _type=inventory.TreeReference):
140
 
    """Convert a tree content record to a TreeReference."""
141
 
    result = _type(file_id, name, parent_id)
142
 
    result.revision = last_modified
143
 
    result.reference_revision = content[1]
144
 
    return result
145
 
 
146
 
 
147
 
class InventoryDeltaSerializer(object):
148
 
    """Serialize inventory deltas."""
149
 
 
150
 
    def __init__(self, versioned_root, tree_references):
151
 
        """Create an InventoryDeltaSerializer.
152
 
 
153
 
        :param versioned_root: If True, any root entry that is seen is expected
154
 
            to be versioned, and root entries can have any fileid.
155
 
        :param tree_references: If True support tree-reference entries.
156
 
        """
157
 
        self._versioned_root = versioned_root
158
 
        self._tree_references = tree_references
159
 
        self._entry_to_content = {
160
 
            'directory': _directory_content,
161
 
            'file': _file_content,
162
 
            'symlink': _link_content,
163
 
        }
164
 
        if tree_references:
165
 
            self._entry_to_content['tree-reference'] = _reference_content
166
 
 
167
 
    def delta_to_lines(self, old_name, new_name, delta_to_new):
168
 
        """Return a line sequence for delta_to_new.
169
 
 
170
 
        Both the versioned_root and tree_references flags must be set via
171
 
        require_flags before calling this.
172
 
 
173
 
        :param old_name: A UTF8 revision id for the old inventory.  May be
174
 
            NULL_REVISION if there is no older inventory and delta_to_new
175
 
            includes the entire inventory contents.
176
 
        :param new_name: The version name of the inventory we create with this
177
 
            delta.
178
 
        :param delta_to_new: An inventory delta such as Inventory.apply_delta
179
 
            takes.
180
 
        :return: The serialized delta as lines.
181
 
        """
182
 
        if not isinstance(old_name, bytes):
183
 
            raise TypeError('old_name should be str, got %r' % (old_name,))
184
 
        if not isinstance(new_name, bytes):
185
 
            raise TypeError('new_name should be str, got %r' % (new_name,))
186
 
        lines = [b'', b'', b'', b'', b'']
187
 
        to_line = self._delta_item_to_line
188
 
        for delta_item in delta_to_new:
189
 
            line = to_line(delta_item, new_name)
190
 
            # GZ 2017-06-09: Not really worth asserting this here
191
 
            if line.__class__ != bytes:
192
 
                raise InventoryDeltaError(
193
 
                    'to_line gave non-bytes output %(line)r', line=lines[-1])
194
 
            lines.append(line)
195
 
        lines.sort()
196
 
        lines[0] = b"format: %s\n" % FORMAT_1
197
 
        lines[1] = b"parent: %s\n" % old_name
198
 
        lines[2] = b"version: %s\n" % new_name
199
 
        lines[3] = b"versioned_root: %s\n" % self._serialize_bool(
200
 
            self._versioned_root)
201
 
        lines[4] = b"tree_references: %s\n" % self._serialize_bool(
202
 
            self._tree_references)
203
 
        return lines
204
 
 
205
 
    def _serialize_bool(self, value):
206
 
        if value:
207
 
            return b"true"
208
 
        else:
209
 
            return b"false"
210
 
 
211
 
    def _delta_item_to_line(self, delta_item, new_version):
212
 
        """Convert delta_item to a line."""
213
 
        oldpath, newpath, file_id, entry = delta_item
214
 
        if newpath is None:
215
 
            # delete
216
 
            oldpath_utf8 = b'/' + oldpath.encode('utf8')
217
 
            newpath_utf8 = b'None'
218
 
            parent_id = b''
219
 
            last_modified = NULL_REVISION
220
 
            content = b'deleted\x00\x00'
221
 
        else:
222
 
            if oldpath is None:
223
 
                oldpath_utf8 = b'None'
224
 
            else:
225
 
                oldpath_utf8 = b'/' + oldpath.encode('utf8')
226
 
            if newpath == '/':
227
 
                raise AssertionError(
228
 
                    "Bad inventory delta: '/' is not a valid newpath "
229
 
                    "(should be '') in delta item %r" % (delta_item,))
230
 
            # TODO: Test real-world utf8 cache hit rate. It may be a win.
231
 
            newpath_utf8 = b'/' + newpath.encode('utf8')
232
 
            # Serialize None as ''
233
 
            parent_id = entry.parent_id or b''
234
 
            # Serialize unknown revisions as NULL_REVISION
235
 
            last_modified = entry.revision
236
 
            # special cases for /
237
 
            if newpath_utf8 == b'/' and not self._versioned_root:
238
 
                # This is an entry for the root, this inventory does not
239
 
                # support versioned roots.  So this must be an unversioned
240
 
                # root, i.e. last_modified == new revision.  Otherwise, this
241
 
                # delta is invalid.
242
 
                # Note: the non-rich-root repositories *can* have roots with
243
 
                # file-ids other than TREE_ROOT, e.g. repo formats that use the
244
 
                # xml5 serializer.
245
 
                if last_modified != new_version:
246
 
                    raise InventoryDeltaError(
247
 
                        'Version present for / in %(fileid)r'
248
 
                        ' (%(last)r != %(new)r)',
249
 
                        fileid=file_id, last=last_modified, new=new_version)
250
 
            if last_modified is None:
251
 
                raise InventoryDeltaError(
252
 
                    "no version for fileid %(fileid)r", fileid=file_id)
253
 
            content = self._entry_to_content[entry.kind](entry)
254
 
        return (b"%s\x00%s\x00%s\x00%s\x00%s\x00%s\n" %
255
 
                (oldpath_utf8, newpath_utf8, file_id, parent_id, last_modified,
256
 
                 content))
257
 
 
258
 
 
259
 
class InventoryDeltaDeserializer(object):
260
 
    """Deserialize inventory deltas."""
261
 
 
262
 
    def __init__(self, allow_versioned_root=True, allow_tree_references=True):
263
 
        """Create an InventoryDeltaDeserializer.
264
 
 
265
 
        :param versioned_root: If True, any root entry that is seen is expected
266
 
            to be versioned, and root entries can have any fileid.
267
 
        :param tree_references: If True support tree-reference entries.
268
 
        """
269
 
        self._allow_versioned_root = allow_versioned_root
270
 
        self._allow_tree_references = allow_tree_references
271
 
 
272
 
    def _deserialize_bool(self, value):
273
 
        if value == b"true":
274
 
            return True
275
 
        elif value == b"false":
276
 
            return False
277
 
        else:
278
 
            raise InventoryDeltaError("value %(val)r is not a bool", val=value)
279
 
 
280
 
    def parse_text_bytes(self, lines):
281
 
        """Parse the text bytes of a serialized inventory delta.
282
 
 
283
 
        If versioned_root and/or tree_references flags were set via
284
 
        require_flags, then the parsed flags must match or a BzrError will be
285
 
        raised.
286
 
 
287
 
        :param lines: The lines to parse. This can be obtained by calling
288
 
            delta_to_lines.
289
 
        :return: (parent_id, new_id, versioned_root, tree_references,
290
 
            inventory_delta)
291
 
        """
292
 
        if not lines:
293
 
            raise InventoryDeltaError(
294
 
                'inventory delta is empty')
295
 
        if not lines[-1].endswith(b'\n'):
296
 
            raise InventoryDeltaError(
297
 
                'last line not empty: %(line)r', line=lines[-1])
298
 
        lines = [line.rstrip(b'\n') for line in lines]  # discard the last empty line
299
 
        if not lines or lines[0] != b'format: %s' % FORMAT_1:
300
 
            raise InventoryDeltaError(
301
 
                'unknown format %(line)r', line=lines[0:1])
302
 
        if len(lines) < 2 or not lines[1].startswith(b'parent: '):
303
 
            raise InventoryDeltaError('missing parent: marker')
304
 
        delta_parent_id = lines[1][8:]
305
 
        if len(lines) < 3 or not lines[2].startswith(b'version: '):
306
 
            raise InventoryDeltaError('missing version: marker')
307
 
        delta_version_id = lines[2][9:]
308
 
        if len(lines) < 4 or not lines[3].startswith(b'versioned_root: '):
309
 
            raise InventoryDeltaError('missing versioned_root: marker')
310
 
        delta_versioned_root = self._deserialize_bool(lines[3][16:])
311
 
        if len(lines) < 5 or not lines[4].startswith(b'tree_references: '):
312
 
            raise InventoryDeltaError('missing tree_references: marker')
313
 
        delta_tree_references = self._deserialize_bool(lines[4][17:])
314
 
        if (not self._allow_versioned_root and delta_versioned_root):
315
 
            raise IncompatibleInventoryDelta("versioned_root not allowed")
316
 
        result = []
317
 
        seen_ids = set()
318
 
        line_iter = iter(lines)
319
 
        for i in range(5):
320
 
            next(line_iter)
321
 
        for line in line_iter:
322
 
            (oldpath_utf8, newpath_utf8, file_id, parent_id, last_modified,
323
 
                content) = line.split(b'\x00', 5)
324
 
            parent_id = parent_id or None
325
 
            if file_id in seen_ids:
326
 
                raise InventoryDeltaError(
327
 
                    "duplicate file id %(fileid)r", fileid=file_id)
328
 
            seen_ids.add(file_id)
329
 
            if (newpath_utf8 == b'/' and not delta_versioned_root and
330
 
                    last_modified != delta_version_id):
331
 
                # Delta claims to be not have a versioned root, yet here's
332
 
                # a root entry with a non-default version.
333
 
                raise InventoryDeltaError(
334
 
                    "Versioned root found: %(line)r", line=line)
335
 
            elif newpath_utf8 != b'None' and last_modified[-1:] == b':':
336
 
                # Deletes have a last_modified of null:, but otherwise special
337
 
                # revision ids should not occur.
338
 
                raise InventoryDeltaError(
339
 
                    'special revisionid found: %(line)r', line=line)
340
 
            if content.startswith(b'tree\x00'):
341
 
                if delta_tree_references is False:
342
 
                    raise InventoryDeltaError(
343
 
                        "Tree reference found (but header said "
344
 
                        "tree_references: false): %(line)r", line=line)
345
 
                elif not self._allow_tree_references:
346
 
                    raise IncompatibleInventoryDelta(
347
 
                        "Tree reference not allowed")
348
 
            if oldpath_utf8 == b'None':
349
 
                oldpath = None
350
 
            elif oldpath_utf8[:1] != b'/':
351
 
                raise InventoryDeltaError(
352
 
                    "oldpath invalid (does not start with /): %(path)r",
353
 
                    path=oldpath_utf8)
354
 
            else:
355
 
                oldpath_utf8 = oldpath_utf8[1:]
356
 
                oldpath = oldpath_utf8.decode('utf8')
357
 
            if newpath_utf8 == b'None':
358
 
                newpath = None
359
 
            elif newpath_utf8[:1] != b'/':
360
 
                raise InventoryDeltaError(
361
 
                    "newpath invalid (does not start with /): %(path)r",
362
 
                    path=newpath_utf8)
363
 
            else:
364
 
                # Trim leading slash
365
 
                newpath_utf8 = newpath_utf8[1:]
366
 
                newpath = newpath_utf8.decode('utf8')
367
 
            content_tuple = tuple(content.split(b'\x00'))
368
 
            if content_tuple[0] == b'deleted':
369
 
                entry = None
370
 
            else:
371
 
                entry = _parse_entry(
372
 
                    newpath, file_id, parent_id, last_modified, content_tuple)
373
 
            delta_item = (oldpath, newpath, file_id, entry)
374
 
            result.append(delta_item)
375
 
        return (delta_parent_id, delta_version_id, delta_versioned_root,
376
 
                delta_tree_references, result)
377
 
 
378
 
 
379
 
def _parse_entry(path, file_id, parent_id, last_modified, content):
380
 
    entry_factory = {
381
 
        b'dir': _dir_to_entry,
382
 
        b'file': _file_to_entry,
383
 
        b'link': _link_to_entry,
384
 
        b'tree': _tree_to_entry,
385
 
    }
386
 
    kind = content[0]
387
 
    if path.startswith('/'):
388
 
        raise AssertionError
389
 
    name = basename(path)
390
 
    return entry_factory[content[0]](
391
 
        content, name, parent_id, file_id, last_modified)