/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 bzrlib/inventory_delta.py

  • Committer: John Arbash Meinel
  • Date: 2009-12-22 16:28:47 UTC
  • mto: This revision was merged to the branch mainline in revision 4922.
  • Revision ID: john@arbash-meinel.com-20091222162847-tvnsc69to4l4uf5r
Implement a permute_for_extension helper.

Use it for all of the 'simple' extension permutations.
It basically permutes all tests in the current module, by setting TestCase.module.
Which works well for most of our extension tests. Some had more advanced
handling of permutations (extra permutations, custom vars, etc.)

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 bzrlib import errors
 
28
from bzrlib.osutils import basename
 
29
from bzrlib import inventory
 
30
from bzrlib.revision import NULL_REVISION
 
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
 
 
51
 
 
52
def _directory_content(entry):
 
53
    """Serialize the content component of entry which is a directory.
 
54
    
 
55
    :param entry: An InventoryDirectory.
 
56
    """
 
57
    return "dir"
 
58
 
 
59
 
 
60
def _file_content(entry):
 
61
    """Serialize the content component of entry which is a file.
 
62
    
 
63
    :param entry: An InventoryFile.
 
64
    """
 
65
    if entry.executable:
 
66
        exec_bytes = 'Y'
 
67
    else:
 
68
        exec_bytes = ''
 
69
    size_exec_sha = (entry.text_size, exec_bytes, entry.text_sha1)
 
70
    if None in size_exec_sha:
 
71
        raise InventoryDeltaError('Missing size or sha for %s' % entry.file_id)
 
72
    return "file\x00%d\x00%s\x00%s" % size_exec_sha
 
73
 
 
74
 
 
75
def _link_content(entry):
 
76
    """Serialize the content component of entry which is a symlink.
 
77
    
 
78
    :param entry: An InventoryLink.
 
79
    """
 
80
    target = entry.symlink_target
 
81
    if target is None:
 
82
        raise InventoryDeltaError('Missing target for %s' % entry.file_id)
 
83
    return "link\x00%s" % target.encode('utf8')
 
84
 
 
85
 
 
86
def _reference_content(entry):
 
87
    """Serialize the content component of entry which is a tree-reference.
 
88
    
 
89
    :param entry: A TreeReference.
 
90
    """
 
91
    tree_revision = entry.reference_revision
 
92
    if tree_revision is None:
 
93
        raise InventoryDeltaError(
 
94
            'Missing reference revision for %s' % entry.file_id)
 
95
    return "tree\x00%s" % tree_revision
 
96
 
 
97
 
 
98
def _dir_to_entry(content, name, parent_id, file_id, last_modified,
 
99
    _type=inventory.InventoryDirectory):
 
100
    """Convert a dir content record to an InventoryDirectory."""
 
101
    result = _type(file_id, name, parent_id)
 
102
    result.revision = last_modified
 
103
    return result
 
104
 
 
105
 
 
106
def _file_to_entry(content, name, parent_id, file_id, last_modified,
 
107
    _type=inventory.InventoryFile):
 
108
    """Convert a dir content record to an InventoryFile."""
 
109
    result = _type(file_id, name, parent_id)
 
110
    result.revision = last_modified
 
111
    result.text_size = int(content[1])
 
112
    result.text_sha1 = content[3]
 
113
    if content[2]:
 
114
        result.executable = True
 
115
    else:
 
116
        result.executable = False
 
117
    return result
 
118
 
 
119
 
 
120
def _link_to_entry(content, name, parent_id, file_id, last_modified,
 
121
    _type=inventory.InventoryLink):
 
122
    """Convert a link content record to an InventoryLink."""
 
123
    result = _type(file_id, name, parent_id)
 
124
    result.revision = last_modified
 
125
    result.symlink_target = content[1].decode('utf8')
 
126
    return result
 
127
 
 
128
 
 
129
def _tree_to_entry(content, name, parent_id, file_id, last_modified,
 
130
    _type=inventory.TreeReference):
 
131
    """Convert a tree content record to a TreeReference."""
 
132
    result = _type(file_id, name, parent_id)
 
133
    result.revision = last_modified
 
134
    result.reference_revision = content[1]
 
135
    return result
 
136
 
 
137
 
 
138
class InventoryDeltaSerializer(object):
 
139
    """Serialize inventory deltas."""
 
140
 
 
141
    def __init__(self, versioned_root, tree_references):
 
142
        """Create an InventoryDeltaSerializer.
 
143
 
 
144
        :param versioned_root: If True, any root entry that is seen is expected
 
145
            to be versioned, and root entries can have any fileid.
 
146
        :param tree_references: If True support tree-reference entries.
 
147
        """
 
148
        self._versioned_root = versioned_root
 
149
        self._tree_references = tree_references
 
150
        self._entry_to_content = {
 
151
            'directory': _directory_content,
 
152
            'file': _file_content,
 
153
            'symlink': _link_content,
 
154
        }
 
155
        if tree_references:
 
156
            self._entry_to_content['tree-reference'] = _reference_content
 
157
 
 
158
    def delta_to_lines(self, old_name, new_name, delta_to_new):
 
159
        """Return a line sequence for delta_to_new.
 
160
 
 
161
        Both the versioned_root and tree_references flags must be set via
 
162
        require_flags before calling this.
 
163
 
 
164
        :param old_name: A UTF8 revision id for the old inventory.  May be
 
165
            NULL_REVISION if there is no older inventory and delta_to_new
 
166
            includes the entire inventory contents.
 
167
        :param new_name: The version name of the inventory we create with this
 
168
            delta.
 
169
        :param delta_to_new: An inventory delta such as Inventory.apply_delta
 
170
            takes.
 
171
        :return: The serialized delta as lines.
 
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,))
 
177
        lines = ['', '', '', '', '']
 
178
        to_line = self._delta_item_to_line
 
179
        for delta_item in delta_to_new:
 
180
            line = to_line(delta_item, new_name)
 
181
            if line.__class__ != str:
 
182
                raise InventoryDeltaError(
 
183
                    'to_line generated non-str output %r' % lines[-1])
 
184
            lines.append(line)
 
185
        lines.sort()
 
186
        lines[0] = "format: %s\n" % FORMAT_1
 
187
        lines[1] = "parent: %s\n" % old_name
 
188
        lines[2] = "version: %s\n" % new_name
 
189
        lines[3] = "versioned_root: %s\n" % self._serialize_bool(
 
190
            self._versioned_root)
 
191
        lines[4] = "tree_references: %s\n" % self._serialize_bool(
 
192
            self._tree_references)
 
193
        return lines
 
194
 
 
195
    def _serialize_bool(self, value):
 
196
        if value:
 
197
            return "true"
 
198
        else:
 
199
            return "false"
 
200
 
 
201
    def _delta_item_to_line(self, delta_item, new_version):
 
202
        """Convert delta_item to a line."""
 
203
        oldpath, newpath, file_id, entry = delta_item
 
204
        if newpath is None:
 
205
            # delete
 
206
            oldpath_utf8 = '/' + oldpath.encode('utf8')
 
207
            newpath_utf8 = 'None'
 
208
            parent_id = ''
 
209
            last_modified = NULL_REVISION
 
210
            content = 'deleted\x00\x00'
 
211
        else:
 
212
            if oldpath is None:
 
213
                oldpath_utf8 = 'None'
 
214
            else:
 
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,))
 
220
            # TODO: Test real-world utf8 cache hit rate. It may be a win.
 
221
            newpath_utf8 = '/' + newpath.encode('utf8')
 
222
            # Serialize None as ''
 
223
            parent_id = entry.parent_id or ''
 
224
            # Serialize unknown revisions as NULL_REVISION
 
225
            last_modified = entry.revision
 
226
            # special cases for /
 
227
            if newpath_utf8 == '/' and not self._versioned_root:
 
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))
 
239
            if last_modified is None:
 
240
                raise InventoryDeltaError("no version for fileid %s" % file_id)
 
241
            content = self._entry_to_content[entry.kind](entry)
 
242
        return ("%s\x00%s\x00%s\x00%s\x00%s\x00%s\n" %
 
243
            (oldpath_utf8, newpath_utf8, file_id, parent_id, last_modified,
 
244
                content))
 
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
 
 
260
    def _deserialize_bool(self, value):
 
261
        if value == "true":
 
262
            return True
 
263
        elif value == "false":
 
264
            return False
 
265
        else:
 
266
            raise InventoryDeltaError("value %r is not a bool" % (value,))
 
267
 
 
268
    def parse_text_bytes(self, bytes):
 
269
        """Parse the text bytes of a serialized inventory delta.
 
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
 
 
275
        :param bytes: The bytes to parse. This can be obtained by calling
 
276
            delta_to_lines and then doing ''.join(delta_lines).
 
277
        :return: (parent_id, new_id, versioned_root, tree_references,
 
278
            inventory_delta)
 
279
        """
 
280
        if bytes[-1:] != '\n':
 
281
            last_line = bytes.rsplit('\n', 1)[-1]
 
282
            raise InventoryDeltaError('last line not empty: %r' % (last_line,))
 
283
        lines = bytes.split('\n')[:-1] # discard the last empty line
 
284
        if not lines or lines[0] != 'format: %s' % FORMAT_1:
 
285
            raise InventoryDeltaError('unknown format %r' % lines[0:1])
 
286
        if len(lines) < 2 or not lines[1].startswith('parent: '):
 
287
            raise InventoryDeltaError('missing parent: marker')
 
288
        delta_parent_id = lines[1][8:]
 
289
        if len(lines) < 3 or not lines[2].startswith('version: '):
 
290
            raise InventoryDeltaError('missing version: marker')
 
291
        delta_version_id = lines[2][9:]
 
292
        if len(lines) < 4 or not lines[3].startswith('versioned_root: '):
 
293
            raise InventoryDeltaError('missing versioned_root: marker')
 
294
        delta_versioned_root = self._deserialize_bool(lines[3][16:])
 
295
        if len(lines) < 5 or not lines[4].startswith('tree_references: '):
 
296
            raise InventoryDeltaError('missing tree_references: marker')
 
297
        delta_tree_references = self._deserialize_bool(lines[4][17:])
 
298
        if (not self._allow_versioned_root and delta_versioned_root):
 
299
            raise IncompatibleInventoryDelta("versioned_root not allowed")
 
300
        result = []
 
301
        seen_ids = set()
 
302
        line_iter = iter(lines)
 
303
        for i in range(5):
 
304
            line_iter.next()
 
305
        for line in line_iter:
 
306
            (oldpath_utf8, newpath_utf8, file_id, parent_id, last_modified,
 
307
                content) = line.split('\x00', 5)
 
308
            parent_id = parent_id or None
 
309
            if file_id in seen_ids:
 
310
                raise InventoryDeltaError(
 
311
                    "duplicate file id in inventory delta %r" % lines)
 
312
            seen_ids.add(file_id)
 
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")
 
330
            if oldpath_utf8 == 'None':
 
331
                oldpath = None
 
332
            elif oldpath_utf8[:1] != '/':
 
333
                raise InventoryDeltaError(
 
334
                    "oldpath invalid (does not start with /): %r"
 
335
                    % (oldpath_utf8,))
 
336
            else:
 
337
                oldpath_utf8 = oldpath_utf8[1:]
 
338
                oldpath = oldpath_utf8.decode('utf8')
 
339
            if newpath_utf8 == 'None':
 
340
                newpath = None
 
341
            elif newpath_utf8[:1] != '/':
 
342
                raise InventoryDeltaError(
 
343
                    "newpath invalid (does not start with /): %r"
 
344
                    % (newpath_utf8,))
 
345
            else:
 
346
                # Trim leading slash
 
347
                newpath_utf8 = newpath_utf8[1:]
 
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)
 
355
            delta_item = (oldpath, newpath, file_id, entry)
 
356
            result.append(delta_item)
 
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):
 
362
    entry_factory = {
 
363
        'dir': _dir_to_entry,
 
364
        'file': _file_to_entry,
 
365
        'link': _link_to_entry,
 
366
        'tree': _tree_to_entry,
 
367
    }
 
368
    kind = content[0]
 
369
    if path.startswith('/'):
 
370
        raise AssertionError
 
371
    name = basename(path)
 
372
    return entry_factory[content[0]](
 
373
            content, name, parent_id, file_id, last_modified)
 
374
 
 
375