~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory_delta.py

  • Committer: John Arbash Meinel
  • Date: 2008-07-09 21:42:24 UTC
  • mto: This revision was merged to the branch mainline in revision 3543.
  • Revision ID: john@arbash-meinel.com-20080709214224-r75k87r6a01pfc3h
Restore a real weave merge to 'bzr merge --weave'.

To do so efficiently, we only add the simple LCAs to the final weave
object, unless we run into complexities with the merge graph.
This gives the same effective result as adding all the texts,
with the advantage of not having to extract all of them.

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