~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory_delta.py

  • Committer: Martin Pool
  • Date: 2005-08-24 08:59:32 UTC
  • Revision ID: mbp@sourcefrog.net-20050824085932-c61f1f1f1c930e13
- Add a simple UIFactory 

  The idea of this is to let a client of bzrlib set some 
  policy about how output is displayed.

  In this revision all that's done is that progress bars
  are constructed by a policy established by the application
  rather than being randomly constructed in the library 
  or passed down the calls.  This avoids progress bars
  popping up while running the test suite and cleans up
  some code.

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