1
# Copyright (C) 2005, 2006, 2007, 2008 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
25
revision as _mod_revision,
28
from bzrlib.xml_serializer import SubElement, Element, Serializer
29
from bzrlib.inventory import ROOT_ID, Inventory, InventoryEntry
30
from bzrlib.revision import Revision
31
from bzrlib.errors import BzrError
38
"'":"'", # FIXME: overkill
43
# A cache of InventoryEntry objects
44
_entry_cache = fifo_cache.FIFOCache(10*1024)
47
def _ensure_utf8_re():
48
"""Make sure the _utf8_re and _unicode_re regexes have been compiled."""
49
global _utf8_re, _unicode_re
51
_utf8_re = re.compile('[&<>\'\"]|[\x80-\xff]+')
52
if _unicode_re is None:
53
_unicode_re = re.compile(u'[&<>\'\"\u0080-\uffff]')
56
def _unicode_escape_replace(match, _map=_xml_escape_map):
57
"""Replace a string of non-ascii, non XML safe characters with their escape
59
This will escape both Standard XML escapes, like <>"', etc.
60
As well as escaping non ascii characters, because ElementTree did.
61
This helps us remain compatible to older versions of bzr. We may change
62
our policy in the future, though.
64
# jam 20060816 Benchmarks show that try/KeyError is faster if you
65
# expect the entity to rarely miss. There is about a 10% difference
66
# in overall time. But if you miss frequently, then if None is much
67
# faster. For our use case, we *rarely* have a revision id, file id
68
# or path name that is unicode. So use try/KeyError.
70
return _map[match.group()]
72
return "&#%d;" % ord(match.group())
75
def _utf8_escape_replace(match, _map=_xml_escape_map):
76
"""Escape utf8 characters into XML safe ones.
78
This uses 2 tricks. It is either escaping "standard" characters, like "&<>,
79
or it is handling characters with the high-bit set. For ascii characters,
80
we just lookup the replacement in the dictionary. For everything else, we
81
decode back into Unicode, and then use the XML escape code.
84
return _map[match.group()]
86
return ''.join('&#%d;' % ord(uni_chr)
87
for uni_chr in match.group().decode('utf8'))
92
def _encode_and_escape(unicode_or_utf8_str, _map=_to_escaped_map):
93
"""Encode the string into utf8, and escape invalid XML characters"""
94
# We frequently get entities we have not seen before, so it is better
95
# to check if None, rather than try/KeyError
96
text = _map.get(unicode_or_utf8_str)
98
if unicode_or_utf8_str.__class__ == unicode:
99
# The alternative policy is to do a regular UTF8 encoding
100
# and then escape only XML meta characters.
101
# Performance is equivalent once you use cache_utf8. *However*
102
# this makes the serialized texts incompatible with old versions
103
# of bzr. So no net gain. (Perhaps the read code would handle utf8
104
# better than entity escapes, but cElementTree seems to do just fine
106
text = str(_unicode_re.sub(_unicode_escape_replace,
107
unicode_or_utf8_str)) + '"'
109
# Plain strings are considered to already be in utf-8 so we do a
110
# slightly different method for escaping.
111
text = _utf8_re.sub(_utf8_escape_replace,
112
unicode_or_utf8_str) + '"'
113
_map[unicode_or_utf8_str] = text
117
def _get_utf8_or_ascii(a_str,
118
_encode_utf8=cache_utf8.encode,
119
_get_cached_ascii=cache_utf8.get_cached_ascii):
120
"""Return a cached version of the string.
122
cElementTree will return a plain string if the XML is plain ascii. It only
123
returns Unicode when it needs to. We want to work in utf-8 strings. So if
124
cElementTree returns a plain string, we can just return the cached version.
125
If it is Unicode, then we need to encode it.
127
:param a_str: An 8-bit string or Unicode as returned by
128
cElementTree.Element.get()
129
:return: A utf-8 encoded 8-bit string.
131
# This is fairly optimized because we know what cElementTree does, this is
132
# not meant as a generic function for all cases. Because it is possible for
133
# an 8-bit string to not be ascii or valid utf8.
134
if a_str.__class__ == unicode:
135
return _encode_utf8(a_str)
137
return _get_cached_ascii(a_str)
141
"""Clean out the unicode => escaped map"""
142
_to_escaped_map.clear()
145
class Serializer_v8(Serializer):
146
"""This serialiser adds rich roots.
148
Its revision format number matches its inventory number.
154
support_altered_by_hack = True
155
# This format supports the altered-by hack that reads file ids directly out
156
# of the versionedfile, without doing XML parsing.
158
supported_kinds = set(['file', 'directory', 'symlink'])
160
revision_format_num = None
162
def _check_revisions(self, inv):
163
"""Extension point for subclasses to check during serialisation.
165
:param inv: An inventory about to be serialised, to be checked.
166
:raises: AssertionError if an error has occured.
168
if inv.revision_id is None:
169
raise AssertionError()
170
if inv.root.revision is None:
171
raise AssertionError()
173
def write_inventory_to_lines(self, inv):
174
"""Return a list of lines with the encoded inventory."""
175
return self.write_inventory(inv, None)
177
def write_inventory_to_string(self, inv, working=False):
178
"""Just call write_inventory with a StringIO and return the value.
180
:param working: If True skip history data - text_sha1, text_size,
181
reference_revision, symlink_target.
183
sio = cStringIO.StringIO()
184
self.write_inventory(inv, sio, working)
185
return sio.getvalue()
187
def write_inventory(self, inv, f, working=False):
188
"""Write inventory to a file.
190
:param inv: the inventory to write.
191
:param f: the file to write. (May be None if the lines are the desired
193
:param working: If True skip history data - text_sha1, text_size,
194
reference_revision, symlink_target.
195
:return: The inventory as a list of lines.
198
self._check_revisions(inv)
200
append = output.append
201
self._append_inventory_root(append, inv)
202
entries = inv.iter_entries()
204
root_path, root_ie = entries.next()
205
for path, ie in entries:
206
if ie.parent_id != self.root_id:
207
parent_str = ' parent_id="'
208
parent_id = _encode_and_escape(ie.parent_id)
212
if ie.kind == 'file':
214
executable = ' executable="yes"'
218
append('<file%s file_id="%s name="%s%s%s revision="%s '
219
'text_sha1="%s" text_size="%d" />\n' % (
220
executable, _encode_and_escape(ie.file_id),
221
_encode_and_escape(ie.name), parent_str, parent_id,
222
_encode_and_escape(ie.revision), ie.text_sha1,
225
append('<file%s file_id="%s name="%s%s%s />\n' % (
226
executable, _encode_and_escape(ie.file_id),
227
_encode_and_escape(ie.name), parent_str, parent_id))
228
elif ie.kind == 'directory':
230
append('<directory file_id="%s name="%s%s%s revision="%s '
232
_encode_and_escape(ie.file_id),
233
_encode_and_escape(ie.name),
234
parent_str, parent_id,
235
_encode_and_escape(ie.revision)))
237
append('<directory file_id="%s name="%s%s%s />\n' % (
238
_encode_and_escape(ie.file_id),
239
_encode_and_escape(ie.name),
240
parent_str, parent_id))
241
elif ie.kind == 'symlink':
243
append('<symlink file_id="%s name="%s%s%s revision="%s '
244
'symlink_target="%s />\n' % (
245
_encode_and_escape(ie.file_id),
246
_encode_and_escape(ie.name),
247
parent_str, parent_id,
248
_encode_and_escape(ie.revision),
249
_encode_and_escape(ie.symlink_target)))
251
append('<symlink file_id="%s name="%s%s%s />\n' % (
252
_encode_and_escape(ie.file_id),
253
_encode_and_escape(ie.name),
254
parent_str, parent_id))
255
elif ie.kind == 'tree-reference':
256
if ie.kind not in self.supported_kinds:
257
raise errors.UnsupportedInventoryKind(ie.kind)
259
append('<tree-reference file_id="%s name="%s%s%s '
260
'revision="%s reference_revision="%s />\n' % (
261
_encode_and_escape(ie.file_id),
262
_encode_and_escape(ie.name),
263
parent_str, parent_id,
264
_encode_and_escape(ie.revision),
265
_encode_and_escape(ie.reference_revision)))
267
append('<tree-reference file_id="%s name="%s%s%s />\n' % (
268
_encode_and_escape(ie.file_id),
269
_encode_and_escape(ie.name),
270
parent_str, parent_id))
272
raise errors.UnsupportedInventoryKind(ie.kind)
273
append('</inventory>\n')
276
# Just to keep the cache from growing without bounds
277
# but we may actually not want to do clear the cache
281
def _append_inventory_root(self, append, inv):
282
"""Append the inventory root to output."""
283
if inv.revision_id is not None:
284
revid1 = ' revision_id="'
285
revid2 = _encode_and_escape(inv.revision_id)
289
append('<inventory format="%s"%s%s>\n' % (
290
self.format_num, revid1, revid2))
291
append('<directory file_id="%s name="%s revision="%s />\n' % (
292
_encode_and_escape(inv.root.file_id),
293
_encode_and_escape(inv.root.name),
294
_encode_and_escape(inv.root.revision)))
296
def _pack_revision(self, rev):
297
"""Revision object -> xml tree"""
298
# For the XML format, we need to write them as Unicode rather than as
299
# utf-8 strings. So that cElementTree can handle properly escaping
301
decode_utf8 = cache_utf8.decode
302
revision_id = rev.revision_id
303
if isinstance(revision_id, str):
304
revision_id = decode_utf8(revision_id)
305
format_num = self.format_num
306
if self.revision_format_num is not None:
307
format_num = self.revision_format_num
308
root = Element('revision',
309
committer = rev.committer,
310
timestamp = '%.3f' % rev.timestamp,
311
revision_id = revision_id,
312
inventory_sha1 = rev.inventory_sha1,
315
if rev.timezone is not None:
316
root.set('timezone', str(rev.timezone))
318
msg = SubElement(root, 'message')
319
msg.text = rev.message
322
pelts = SubElement(root, 'parents')
323
pelts.tail = pelts.text = '\n'
324
for parent_id in rev.parent_ids:
325
_mod_revision.check_not_reserved_id(parent_id)
326
p = SubElement(pelts, 'revision_ref')
328
if isinstance(parent_id, str):
329
parent_id = decode_utf8(parent_id)
330
p.set('revision_id', parent_id)
332
self._pack_revision_properties(rev, root)
335
def _pack_revision_properties(self, rev, under_element):
336
top_elt = SubElement(under_element, 'properties')
337
for prop_name, prop_value in sorted(rev.properties.items()):
338
prop_elt = SubElement(top_elt, 'property')
339
prop_elt.set('name', prop_name)
340
prop_elt.text = prop_value
344
def _unpack_inventory(self, elt, revision_id=None):
345
"""Construct from XML Element"""
346
if elt.tag != 'inventory':
347
raise errors.UnexpectedInventoryFormat('Root tag is %r' % elt.tag)
348
format = elt.get('format')
349
if format != self.format_num:
350
raise errors.UnexpectedInventoryFormat('Invalid format version %r'
352
revision_id = elt.get('revision_id')
353
if revision_id is not None:
354
revision_id = cache_utf8.encode(revision_id)
355
inv = inventory.Inventory(root_id=None, revision_id=revision_id)
357
ie = self._unpack_entry(e)
361
def _unpack_entry(self, elt):
362
get_cached = _get_utf8_or_ascii
365
file_id = elt_get('file_id')
366
revision = elt_get('revision')
367
# Check and see if we have already unpacked this exact entry
368
# Some timings for "repo.revision_trees(last_100_bzr_revs)"
374
# Note that a cache of 10k nodes is more than sufficient to hold all of
375
# the inventory for the last 100 revs.
376
# With inventory.add() optimizations, and not copying file entries,
377
# performance gets to 2.00s
378
key = (file_id, revision)
380
# We copy it, because some operatations may mutate it
381
cached_ie = _entry_cache[key]
385
# Only copying directory entries drops us 2.85s => 2.35s
386
if cached_ie.kind == 'directory':
387
return cached_ie.copy()
389
# return cached_ie.copy()
392
if not InventoryEntry.versionable_kind(kind):
393
raise AssertionError('unsupported entry kind %s' % kind)
395
file_id = get_cached(file_id)
396
if revision is not None:
397
revision = get_cached(revision)
398
parent_id = elt_get('parent_id')
399
if parent_id is not None:
400
parent_id = get_cached(parent_id)
402
if kind == 'directory':
403
ie = inventory.InventoryDirectory(file_id,
407
ie = inventory.InventoryFile(file_id,
410
ie.text_sha1 = elt_get('text_sha1')
411
if elt_get('executable') == 'yes':
413
v = elt_get('text_size')
414
ie.text_size = v and int(v)
415
elif kind == 'symlink':
416
ie = inventory.InventoryLink(file_id,
419
ie.symlink_target = elt_get('symlink_target')
421
raise errors.UnsupportedInventoryKind(kind)
422
ie.revision = revision
423
if revision is not None:
424
_entry_cache[key] = ie
428
def _unpack_revision(self, elt):
429
"""XML Element -> Revision object"""
430
format = elt.get('format')
431
format_num = self.format_num
432
if self.revision_format_num is not None:
433
format_num = self.revision_format_num
434
if format is not None:
435
if format != format_num:
436
raise BzrError("invalid format version %r on revision"
438
get_cached = _get_utf8_or_ascii
439
rev = Revision(committer = elt.get('committer'),
440
timestamp = float(elt.get('timestamp')),
441
revision_id = get_cached(elt.get('revision_id')),
442
inventory_sha1 = elt.get('inventory_sha1')
444
parents = elt.find('parents') or []
446
rev.parent_ids.append(get_cached(p.get('revision_id')))
447
self._unpack_revision_properties(elt, rev)
448
v = elt.get('timezone')
452
rev.timezone = int(v)
453
rev.message = elt.findtext('message') # text of <message>
456
def _unpack_revision_properties(self, elt, rev):
457
"""Unpack properties onto a revision."""
458
props_elt = elt.find('properties')
461
for prop_elt in props_elt:
462
if prop_elt.tag != 'property':
463
raise AssertionError(
464
"bad tag under properties list: %r" % prop_elt.tag)
465
name = prop_elt.get('name')
466
value = prop_elt.text
467
# If a property had an empty value ('') cElementTree reads
468
# that back as None, convert it back to '', so that all
469
# properties have string values
472
if name in rev.properties:
473
raise AssertionError("repeated property %r" % name)
474
rev.properties[name] = value
477
serializer_v8 = Serializer_v8()