/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

  • Committer: Gustav Hartvigsson
  • Date: 2021-01-09 21:36:27 UTC
  • Revision ID: gustav.hartvigsson@gmail.com-20210109213627-h1xwcutzy9m7a99b
Added 'Case Preserving Working Tree Use Cases' from Canonical Wiki

* Addod a page from the Canonical Bazaar wiki
  with information on the scmeatics of case
  perserving filesystems an a case insensitive
  filesystem works.
  
  * Needs re-work, but this will do as it is the
    same inforamoton as what was on the linked
    page in the currint documentation.

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