1
# Copyright (C) 2008, 2009 Canonical Ltd
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.
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.
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
17
"""Inventory delta serialisation.
19
See doc/developers/inventory.txt for the description of the format.
21
In this module the interesting classes are:
22
- InventoryDeltaSerializer - object to read/write inventory deltas.
25
__all__ = ['InventoryDeltaSerializer']
27
from bzrlib import errors
28
from bzrlib.osutils import basename
29
from bzrlib import inventory
30
from bzrlib.revision import NULL_REVISION
33
def _directory_content(entry):
34
"""Serialize the content component of entry which is a directory.
36
:param entry: An InventoryDirectory.
41
def _file_content(entry):
42
"""Serialize the content component of entry which is a file.
44
:param entry: An InventoryFile.
50
size_exec_sha = (entry.text_size, exec_bytes, entry.text_sha1)
51
if None in size_exec_sha:
52
raise errors.BzrError('Missing size or sha for %s' % entry.file_id)
53
return "file\x00%d\x00%s\x00%s" % size_exec_sha
56
def _link_content(entry):
57
"""Serialize the content component of entry which is a symlink.
59
:param entry: An InventoryLink.
61
target = entry.symlink_target
63
raise errors.BzrError('Missing target for %s' % entry.file_id)
64
return "link\x00%s" % target.encode('utf8')
67
def _reference_content(entry):
68
"""Serialize the content component of entry which is a tree-reference.
70
:param entry: A TreeReference.
72
tree_revision = entry.reference_revision
73
if tree_revision is None:
74
raise errors.BzrError('Missing reference revision for %s' % entry.file_id)
75
return "tree\x00%s" % tree_revision
78
def _dir_to_entry(content, name, parent_id, file_id, last_modified,
79
_type=inventory.InventoryDirectory):
80
"""Convert a dir content record to an InventoryDirectory."""
81
result = _type(file_id, name, parent_id)
82
result.revision = last_modified
86
def _file_to_entry(content, name, parent_id, file_id, last_modified,
87
_type=inventory.InventoryFile):
88
"""Convert a dir content record to an InventoryFile."""
89
result = _type(file_id, name, parent_id)
90
result.revision = last_modified
91
result.text_size = int(content[1])
92
result.text_sha1 = content[3]
94
result.executable = True
96
result.executable = False
100
def _link_to_entry(content, name, parent_id, file_id, last_modified,
101
_type=inventory.InventoryLink):
102
"""Convert a link content record to an InventoryLink."""
103
result = _type(file_id, name, parent_id)
104
result.revision = last_modified
105
result.symlink_target = content[1].decode('utf8')
109
def _tree_to_entry(content, name, parent_id, file_id, last_modified,
110
_type=inventory.TreeReference):
111
"""Convert a tree content record to a TreeReference."""
112
result = _type(file_id, name, parent_id)
113
result.revision = last_modified
114
result.reference_revision = content[1]
119
class InventoryDeltaSerializer(object):
120
"""Serialize and deserialize inventory deltas."""
122
FORMAT_1 = 'bzr inventory delta v1 (bzr 1.14)'
124
def __init__(self, versioned_root, tree_references):
125
"""Create an InventoryDeltaSerializer.
127
:param versioned_root: If True, any root entry that is seen is expected
128
to be versioned, and root entries can have any fileid.
129
:param tree_references: If True support tree-reference entries.
131
self._versioned_root = versioned_root
132
self._tree_references = tree_references
133
self._entry_to_content = {
134
'directory': _directory_content,
135
'file': _file_content,
136
'symlink': _link_content,
139
self._entry_to_content['tree-reference'] = _reference_content
141
def delta_to_lines(self, old_name, new_name, delta_to_new):
142
"""Return a line sequence for delta_to_new.
144
:param old_name: A UTF8 revision id for the old inventory. May be
145
NULL_REVISION if there is no older inventory and delta_to_new
146
includes the entire inventory contents.
147
:param new_name: The version name of the inventory we create with this
149
:param delta_to_new: An inventory delta such as Inventory.apply_delta
151
:return: The serialized delta as lines.
153
lines = ['', '', '', '', '']
154
to_line = self._delta_item_to_line
155
for delta_item in delta_to_new:
156
lines.append(to_line(delta_item))
157
if lines[-1].__class__ != str:
158
raise errors.BzrError(
159
'to_line generated non-str output %r' % lines[-1])
161
lines[0] = "format: %s\n" % InventoryDeltaSerializer.FORMAT_1
162
lines[1] = "parent: %s\n" % old_name
163
lines[2] = "version: %s\n" % new_name
164
lines[3] = "versioned_root: %s\n" % self._serialize_bool(
165
self._versioned_root)
166
lines[4] = "tree_references: %s\n" % self._serialize_bool(
167
self._tree_references)
170
def _serialize_bool(self, value):
176
def _delta_item_to_line(self, delta_item):
177
"""Convert delta_item to a line."""
178
oldpath, newpath, file_id, entry = delta_item
181
oldpath_utf8 = '/' + oldpath.encode('utf8')
182
newpath_utf8 = 'None'
184
last_modified = NULL_REVISION
185
content = 'deleted\x00\x00'
188
oldpath_utf8 = 'None'
190
oldpath_utf8 = '/' + oldpath.encode('utf8')
191
# TODO: Test real-world utf8 cache hit rate. It may be a win.
192
newpath_utf8 = '/' + newpath.encode('utf8')
193
# Serialize None as ''
194
parent_id = entry.parent_id or ''
195
# Serialize unknown revisions as NULL_REVISION
196
last_modified = entry.revision
197
# special cases for /
198
if newpath_utf8 == '/' and not self._versioned_root:
199
if file_id != 'TREE_ROOT':
200
raise errors.BzrError(
201
'file_id %s is not TREE_ROOT for /' % file_id)
202
if last_modified is not None:
203
raise errors.BzrError(
204
'Version present for / in %s' % file_id)
205
last_modified = NULL_REVISION
206
if last_modified is None:
207
raise errors.BzrError("no version for fileid %s" % file_id)
208
content = self._entry_to_content[entry.kind](entry)
209
return ("%s\x00%s\x00%s\x00%s\x00%s\x00%s\n" %
210
(oldpath_utf8, newpath_utf8, file_id, parent_id, last_modified,
213
def _deserialize_bool(self, value):
216
elif value == "false":
219
raise errors.BzrError("value %r is not a bool" % (value,))
221
def parse_text_bytes(self, bytes):
222
"""Parse the text bytes of a serialized inventory delta.
224
:param bytes: The bytes to parse. This can be obtained by calling
225
delta_to_lines and then doing ''.join(delta_lines).
226
:return: (parent_id, new_id, inventory_delta)
228
lines = bytes.split('\n')[:-1] # discard the last empty line
229
if not lines or lines[0] != 'format: %s' % InventoryDeltaSerializer.FORMAT_1:
230
raise errors.BzrError('unknown format %r' % lines[0:1])
231
if len(lines) < 2 or not lines[1].startswith('parent: '):
232
raise errors.BzrError('missing parent: marker')
233
delta_parent_id = lines[1][8:]
234
if len(lines) < 3 or not lines[2].startswith('version: '):
235
raise errors.BzrError('missing version: marker')
236
delta_version_id = lines[2][9:]
237
if len(lines) < 4 or not lines[3].startswith('versioned_root: '):
238
raise errors.BzrError('missing versioned_root: marker')
239
delta_versioned_root = self._deserialize_bool(lines[3][16:])
240
if len(lines) < 5 or not lines[4].startswith('tree_references: '):
241
raise errors.BzrError('missing tree_references: marker')
242
delta_tree_references = self._deserialize_bool(lines[4][17:])
243
if delta_versioned_root != self._versioned_root:
244
raise errors.BzrError(
245
"serialized versioned_root flag is wrong: %s" %
246
(delta_versioned_root,))
247
if delta_tree_references != self._tree_references:
248
raise errors.BzrError(
249
"serialized tree_references flag is wrong: %s" %
250
(delta_tree_references,))
253
line_iter = iter(lines)
256
for line in line_iter:
257
(oldpath_utf8, newpath_utf8, file_id, parent_id, last_modified,
258
content) = line.split('\x00', 5)
259
parent_id = parent_id or None
260
if file_id in seen_ids:
261
raise errors.BzrError(
262
"duplicate file id in inventory delta %r" % lines)
263
seen_ids.add(file_id)
264
if newpath_utf8 == '/' and not delta_versioned_root and (
265
last_modified != 'null:' or file_id != 'TREE_ROOT'):
266
raise errors.BzrError("Versioned root found: %r" % line)
267
elif last_modified[-1] == ':':
268
raise errors.BzrError('special revisionid found: %r' % line)
269
if not delta_tree_references and content.startswith('tree\x00'):
270
raise errors.BzrError("Tree reference found: %r" % line)
271
content_tuple = tuple(content.split('\x00'))
272
entry = _parse_entry(
273
newpath_utf8, file_id, parent_id, last_modified, content_tuple)
274
if oldpath_utf8 == 'None':
277
oldpath = oldpath_utf8.decode('utf8')
278
if newpath_utf8 == 'None':
281
newpath = newpath_utf8.decode('utf8')
282
delta_item = (oldpath, newpath, file_id, entry)
283
result.append(delta_item)
284
return delta_parent_id, delta_version_id, result
287
def _parse_entry(utf8_path, file_id, parent_id, last_modified, content):
289
'dir': _dir_to_entry,
290
'file': _file_to_entry,
291
'link': _link_to_entry,
292
'tree': _tree_to_entry,
295
path = utf8_path[1:].decode('utf8')
296
name = basename(path)
297
return entry_factory[content[0]](
298
content, name, parent_id, file_id, last_modified)