~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/xml8.py

Initial stab at repository format support.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005-2010 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
 
import cStringIO
18
 
import re
19
 
 
20
 
from bzrlib import (
21
 
    cache_utf8,
22
 
    errors,
23
 
    inventory,
24
 
    lazy_regex,
25
 
    revision as _mod_revision,
26
 
    trace,
27
 
    )
28
 
from bzrlib.xml_serializer import (
29
 
    Element,
30
 
    SubElement,
31
 
    XMLSerializer,
32
 
    escape_invalid_chars,
33
 
    )
34
 
from bzrlib.inventory import InventoryEntry
35
 
from bzrlib.revision import Revision
36
 
from bzrlib.errors import BzrError
37
 
 
38
 
 
39
 
_utf8_re = None
40
 
_unicode_re = None
41
 
_xml_escape_map = {
42
 
    "&":'&',
43
 
    "'":"'", # FIXME: overkill
44
 
    "\"":""",
45
 
    "<":"&lt;",
46
 
    ">":"&gt;",
47
 
    }
48
 
 
49
 
_xml_unescape_map = {
50
 
    'apos':"'",
51
 
    'quot':'"',
52
 
    'amp':'&',
53
 
    'lt':'<',
54
 
    'gt':'>'
55
 
}
56
 
 
57
 
 
58
 
def _unescaper(match, _map=_xml_unescape_map):
59
 
    code = match.group(1)
60
 
    try:
61
 
        return _map[code]
62
 
    except KeyError:
63
 
        if not code.startswith('#'):
64
 
            raise
65
 
        return unichr(int(code[1:])).encode('utf8')
66
 
 
67
 
 
68
 
_unescape_re = None
69
 
 
70
 
 
71
 
def _unescape_xml(data):
72
 
    """Unescape predefined XML entities in a string of data."""
73
 
    global _unescape_re
74
 
    if _unescape_re is None:
75
 
        _unescape_re = re.compile('\&([^;]*);')
76
 
    return _unescape_re.sub(_unescaper, data)
77
 
 
78
 
 
79
 
def _ensure_utf8_re():
80
 
    """Make sure the _utf8_re and _unicode_re regexes have been compiled."""
81
 
    global _utf8_re, _unicode_re
82
 
    if _utf8_re is None:
83
 
        _utf8_re = re.compile('[&<>\'\"]|[\x80-\xff]+')
84
 
    if _unicode_re is None:
85
 
        _unicode_re = re.compile(u'[&<>\'\"\u0080-\uffff]')
86
 
 
87
 
 
88
 
def _unicode_escape_replace(match, _map=_xml_escape_map):
89
 
    """Replace a string of non-ascii, non XML safe characters with their escape
90
 
 
91
 
    This will escape both Standard XML escapes, like <>"', etc.
92
 
    As well as escaping non ascii characters, because ElementTree did.
93
 
    This helps us remain compatible to older versions of bzr. We may change
94
 
    our policy in the future, though.
95
 
    """
96
 
    # jam 20060816 Benchmarks show that try/KeyError is faster if you
97
 
    # expect the entity to rarely miss. There is about a 10% difference
98
 
    # in overall time. But if you miss frequently, then if None is much
99
 
    # faster. For our use case, we *rarely* have a revision id, file id
100
 
    # or path name that is unicode. So use try/KeyError.
101
 
    try:
102
 
        return _map[match.group()]
103
 
    except KeyError:
104
 
        return "&#%d;" % ord(match.group())
105
 
 
106
 
 
107
 
def _utf8_escape_replace(match, _map=_xml_escape_map):
108
 
    """Escape utf8 characters into XML safe ones.
109
 
 
110
 
    This uses 2 tricks. It is either escaping "standard" characters, like "&<>,
111
 
    or it is handling characters with the high-bit set. For ascii characters,
112
 
    we just lookup the replacement in the dictionary. For everything else, we
113
 
    decode back into Unicode, and then use the XML escape code.
114
 
    """
115
 
    try:
116
 
        return _map[match.group()]
117
 
    except KeyError:
118
 
        return ''.join('&#%d;' % ord(uni_chr)
119
 
                       for uni_chr in match.group().decode('utf8'))
120
 
 
121
 
 
122
 
_to_escaped_map = {}
123
 
 
124
 
def _encode_and_escape(unicode_or_utf8_str, _map=_to_escaped_map):
125
 
    """Encode the string into utf8, and escape invalid XML characters"""
126
 
    # We frequently get entities we have not seen before, so it is better
127
 
    # to check if None, rather than try/KeyError
128
 
    text = _map.get(unicode_or_utf8_str)
129
 
    if text is None:
130
 
        if unicode_or_utf8_str.__class__ is unicode:
131
 
            # The alternative policy is to do a regular UTF8 encoding
132
 
            # and then escape only XML meta characters.
133
 
            # Performance is equivalent once you use cache_utf8. *However*
134
 
            # this makes the serialized texts incompatible with old versions
135
 
            # of bzr. So no net gain. (Perhaps the read code would handle utf8
136
 
            # better than entity escapes, but cElementTree seems to do just fine
137
 
            # either way)
138
 
            text = str(_unicode_re.sub(_unicode_escape_replace,
139
 
                                       unicode_or_utf8_str)) + '"'
140
 
        else:
141
 
            # Plain strings are considered to already be in utf-8 so we do a
142
 
            # slightly different method for escaping.
143
 
            text = _utf8_re.sub(_utf8_escape_replace,
144
 
                                unicode_or_utf8_str) + '"'
145
 
        _map[unicode_or_utf8_str] = text
146
 
    return text
147
 
 
148
 
 
149
 
def _get_utf8_or_ascii(a_str,
150
 
                       _encode_utf8=cache_utf8.encode,
151
 
                       _get_cached_ascii=cache_utf8.get_cached_ascii):
152
 
    """Return a cached version of the string.
153
 
 
154
 
    cElementTree will return a plain string if the XML is plain ascii. It only
155
 
    returns Unicode when it needs to. We want to work in utf-8 strings. So if
156
 
    cElementTree returns a plain string, we can just return the cached version.
157
 
    If it is Unicode, then we need to encode it.
158
 
 
159
 
    :param a_str: An 8-bit string or Unicode as returned by
160
 
                  cElementTree.Element.get()
161
 
    :return: A utf-8 encoded 8-bit string.
162
 
    """
163
 
    # This is fairly optimized because we know what cElementTree does, this is
164
 
    # not meant as a generic function for all cases. Because it is possible for
165
 
    # an 8-bit string to not be ascii or valid utf8.
166
 
    if a_str.__class__ is unicode:
167
 
        return _encode_utf8(a_str)
168
 
    else:
169
 
        return intern(a_str)
170
 
 
171
 
 
172
 
def _clear_cache():
173
 
    """Clean out the unicode => escaped map"""
174
 
    _to_escaped_map.clear()
175
 
 
176
 
 
177
 
class Serializer_v8(XMLSerializer):
178
 
    """This serialiser adds rich roots.
179
 
 
180
 
    Its revision format number matches its inventory number.
181
 
    """
182
 
 
183
 
    __slots__ = []
184
 
 
185
 
    root_id = None
186
 
    support_altered_by_hack = True
187
 
    # This format supports the altered-by hack that reads file ids directly out
188
 
    # of the versionedfile, without doing XML parsing.
189
 
 
190
 
    supported_kinds = set(['file', 'directory', 'symlink'])
191
 
    format_num = '8'
192
 
    revision_format_num = None
193
 
 
194
 
    # The search regex used by xml based repositories to determine what things
195
 
    # where changed in a single commit.
196
 
    _file_ids_altered_regex = lazy_regex.lazy_compile(
197
 
        r'file_id="(?P<file_id>[^"]+)"'
198
 
        r'.* revision="(?P<revision_id>[^"]+)"'
199
 
        )
200
 
 
201
 
    def _check_revisions(self, inv):
202
 
        """Extension point for subclasses to check during serialisation.
203
 
 
204
 
        :param inv: An inventory about to be serialised, to be checked.
205
 
        :raises: AssertionError if an error has occurred.
206
 
        """
207
 
        if inv.revision_id is None:
208
 
            raise AssertionError("inv.revision_id is None")
209
 
        if inv.root.revision is None:
210
 
            raise AssertionError("inv.root.revision is None")
211
 
 
212
 
    def _check_cache_size(self, inv_size, entry_cache):
213
 
        """Check that the entry_cache is large enough.
214
 
 
215
 
        We want the cache to be ~2x the size of an inventory. The reason is
216
 
        because we use a FIFO cache, and how Inventory records are likely to
217
 
        change. In general, you have a small number of records which change
218
 
        often, and a lot of records which do not change at all. So when the
219
 
        cache gets full, you actually flush out a lot of the records you are
220
 
        interested in, which means you need to recreate all of those records.
221
 
        An LRU Cache would be better, but the overhead negates the cache
222
 
        coherency benefit.
223
 
 
224
 
        One way to look at it, only the size of the cache > len(inv) is your
225
 
        'working' set. And in general, it shouldn't be a problem to hold 2
226
 
        inventories in memory anyway.
227
 
 
228
 
        :param inv_size: The number of entries in an inventory.
229
 
        """
230
 
        if entry_cache is None:
231
 
            return
232
 
        # 1.5 times might also be reasonable.
233
 
        recommended_min_cache_size = inv_size * 1.5
234
 
        if entry_cache.cache_size() < recommended_min_cache_size:
235
 
            recommended_cache_size = inv_size * 2
236
 
            trace.mutter('Resizing the inventory entry cache from %d to %d',
237
 
                         entry_cache.cache_size(), recommended_cache_size)
238
 
            entry_cache.resize(recommended_cache_size)
239
 
 
240
 
    def write_inventory_to_lines(self, inv):
241
 
        """Return a list of lines with the encoded inventory."""
242
 
        return self.write_inventory(inv, None)
243
 
 
244
 
    def write_inventory_to_string(self, inv, working=False):
245
 
        """Just call write_inventory with a StringIO and return the value.
246
 
 
247
 
        :param working: If True skip history data - text_sha1, text_size,
248
 
            reference_revision, symlink_target.
249
 
        """
250
 
        sio = cStringIO.StringIO()
251
 
        self.write_inventory(inv, sio, working)
252
 
        return sio.getvalue()
253
 
 
254
 
    def write_inventory(self, inv, f, working=False):
255
 
        """Write inventory to a file.
256
 
 
257
 
        :param inv: the inventory to write.
258
 
        :param f: the file to write. (May be None if the lines are the desired
259
 
            output).
260
 
        :param working: If True skip history data - text_sha1, text_size,
261
 
            reference_revision, symlink_target.
262
 
        :return: The inventory as a list of lines.
263
 
        """
264
 
        _ensure_utf8_re()
265
 
        self._check_revisions(inv)
266
 
        output = []
267
 
        append = output.append
268
 
        self._append_inventory_root(append, inv)
269
 
        entries = inv.iter_entries()
270
 
        # Skip the root
271
 
        root_path, root_ie = entries.next()
272
 
        for path, ie in entries:
273
 
            if ie.parent_id != self.root_id:
274
 
                parent_str = ' parent_id="'
275
 
                parent_id  = _encode_and_escape(ie.parent_id)
276
 
            else:
277
 
                parent_str = ''
278
 
                parent_id  = ''
279
 
            if ie.kind == 'file':
280
 
                if ie.executable:
281
 
                    executable = ' executable="yes"'
282
 
                else:
283
 
                    executable = ''
284
 
                if not working:
285
 
                    append('<file%s file_id="%s name="%s%s%s revision="%s '
286
 
                        'text_sha1="%s" text_size="%d" />\n' % (
287
 
                        executable, _encode_and_escape(ie.file_id),
288
 
                        _encode_and_escape(ie.name), parent_str, parent_id,
289
 
                        _encode_and_escape(ie.revision), ie.text_sha1,
290
 
                        ie.text_size))
291
 
                else:
292
 
                    append('<file%s file_id="%s name="%s%s%s />\n' % (
293
 
                        executable, _encode_and_escape(ie.file_id),
294
 
                        _encode_and_escape(ie.name), parent_str, parent_id))
295
 
            elif ie.kind == 'directory':
296
 
                if not working:
297
 
                    append('<directory file_id="%s name="%s%s%s revision="%s '
298
 
                        '/>\n' % (
299
 
                        _encode_and_escape(ie.file_id),
300
 
                        _encode_and_escape(ie.name),
301
 
                        parent_str, parent_id,
302
 
                        _encode_and_escape(ie.revision)))
303
 
                else:
304
 
                    append('<directory file_id="%s name="%s%s%s />\n' % (
305
 
                        _encode_and_escape(ie.file_id),
306
 
                        _encode_and_escape(ie.name),
307
 
                        parent_str, parent_id))
308
 
            elif ie.kind == 'symlink':
309
 
                if not working:
310
 
                    append('<symlink file_id="%s name="%s%s%s revision="%s '
311
 
                        'symlink_target="%s />\n' % (
312
 
                        _encode_and_escape(ie.file_id),
313
 
                        _encode_and_escape(ie.name),
314
 
                        parent_str, parent_id,
315
 
                        _encode_and_escape(ie.revision),
316
 
                        _encode_and_escape(ie.symlink_target)))
317
 
                else:
318
 
                    append('<symlink file_id="%s name="%s%s%s />\n' % (
319
 
                        _encode_and_escape(ie.file_id),
320
 
                        _encode_and_escape(ie.name),
321
 
                        parent_str, parent_id))
322
 
            elif ie.kind == 'tree-reference':
323
 
                if ie.kind not in self.supported_kinds:
324
 
                    raise errors.UnsupportedInventoryKind(ie.kind)
325
 
                if not working:
326
 
                    append('<tree-reference file_id="%s name="%s%s%s '
327
 
                        'revision="%s reference_revision="%s />\n' % (
328
 
                        _encode_and_escape(ie.file_id),
329
 
                        _encode_and_escape(ie.name),
330
 
                        parent_str, parent_id,
331
 
                        _encode_and_escape(ie.revision),
332
 
                        _encode_and_escape(ie.reference_revision)))
333
 
                else:
334
 
                    append('<tree-reference file_id="%s name="%s%s%s />\n' % (
335
 
                        _encode_and_escape(ie.file_id),
336
 
                        _encode_and_escape(ie.name),
337
 
                        parent_str, parent_id))
338
 
            else:
339
 
                raise errors.UnsupportedInventoryKind(ie.kind)
340
 
        append('</inventory>\n')
341
 
        if f is not None:
342
 
            f.writelines(output)
343
 
        # Just to keep the cache from growing without bounds
344
 
        # but we may actually not want to do clear the cache
345
 
        #_clear_cache()
346
 
        return output
347
 
 
348
 
    def _append_inventory_root(self, append, inv):
349
 
        """Append the inventory root to output."""
350
 
        if inv.revision_id is not None:
351
 
            revid1 = ' revision_id="'
352
 
            revid2 = _encode_and_escape(inv.revision_id)
353
 
        else:
354
 
            revid1 = ""
355
 
            revid2 = ""
356
 
        append('<inventory format="%s"%s%s>\n' % (
357
 
            self.format_num, revid1, revid2))
358
 
        append('<directory file_id="%s name="%s revision="%s />\n' % (
359
 
            _encode_and_escape(inv.root.file_id),
360
 
            _encode_and_escape(inv.root.name),
361
 
            _encode_and_escape(inv.root.revision)))
362
 
 
363
 
    def _pack_revision(self, rev):
364
 
        """Revision object -> xml tree"""
365
 
        # For the XML format, we need to write them as Unicode rather than as
366
 
        # utf-8 strings. So that cElementTree can handle properly escaping
367
 
        # them.
368
 
        decode_utf8 = cache_utf8.decode
369
 
        revision_id = rev.revision_id
370
 
        if isinstance(revision_id, str):
371
 
            revision_id = decode_utf8(revision_id)
372
 
        format_num = self.format_num
373
 
        if self.revision_format_num is not None:
374
 
            format_num = self.revision_format_num
375
 
        root = Element('revision',
376
 
                       committer = rev.committer,
377
 
                       timestamp = '%.3f' % rev.timestamp,
378
 
                       revision_id = revision_id,
379
 
                       inventory_sha1 = rev.inventory_sha1,
380
 
                       format=format_num,
381
 
                       )
382
 
        if rev.timezone is not None:
383
 
            root.set('timezone', str(rev.timezone))
384
 
        root.text = '\n'
385
 
        msg = SubElement(root, 'message')
386
 
        msg.text = escape_invalid_chars(rev.message)[0]
387
 
        msg.tail = '\n'
388
 
        if rev.parent_ids:
389
 
            pelts = SubElement(root, 'parents')
390
 
            pelts.tail = pelts.text = '\n'
391
 
            for parent_id in rev.parent_ids:
392
 
                _mod_revision.check_not_reserved_id(parent_id)
393
 
                p = SubElement(pelts, 'revision_ref')
394
 
                p.tail = '\n'
395
 
                if isinstance(parent_id, str):
396
 
                    parent_id = decode_utf8(parent_id)
397
 
                p.set('revision_id', parent_id)
398
 
        if rev.properties:
399
 
            self._pack_revision_properties(rev, root)
400
 
        return root
401
 
 
402
 
    def _pack_revision_properties(self, rev, under_element):
403
 
        top_elt = SubElement(under_element, 'properties')
404
 
        for prop_name, prop_value in sorted(rev.properties.items()):
405
 
            prop_elt = SubElement(top_elt, 'property')
406
 
            prop_elt.set('name', prop_name)
407
 
            prop_elt.text = prop_value
408
 
            prop_elt.tail = '\n'
409
 
        top_elt.tail = '\n'
410
 
 
411
 
    def _unpack_inventory(self, elt, revision_id=None, entry_cache=None,
412
 
                          return_from_cache=False):
413
 
        """Construct from XML Element"""
414
 
        if elt.tag != 'inventory':
415
 
            raise errors.UnexpectedInventoryFormat('Root tag is %r' % elt.tag)
416
 
        format = elt.get('format')
417
 
        if format != self.format_num:
418
 
            raise errors.UnexpectedInventoryFormat('Invalid format version %r'
419
 
                                                   % format)
420
 
        revision_id = elt.get('revision_id')
421
 
        if revision_id is not None:
422
 
            revision_id = cache_utf8.encode(revision_id)
423
 
        inv = inventory.Inventory(root_id=None, revision_id=revision_id)
424
 
        for e in elt:
425
 
            ie = self._unpack_entry(e, entry_cache=entry_cache,
426
 
                                    return_from_cache=return_from_cache)
427
 
            inv.add(ie)
428
 
        self._check_cache_size(len(inv), entry_cache)
429
 
        return inv
430
 
 
431
 
    def _unpack_entry(self, elt, entry_cache=None, return_from_cache=False):
432
 
        elt_get = elt.get
433
 
        file_id = elt_get('file_id')
434
 
        revision = elt_get('revision')
435
 
        # Check and see if we have already unpacked this exact entry
436
 
        # Some timings for "repo.revision_trees(last_100_revs)"
437
 
        #               bzr     mysql
438
 
        #   unmodified  4.1s    40.8s
439
 
        #   using lru   3.5s
440
 
        #   using fifo  2.83s   29.1s
441
 
        #   lru._cache  2.8s
442
 
        #   dict        2.75s   26.8s
443
 
        #   inv.add     2.5s    26.0s
444
 
        #   no_copy     2.00s   20.5s
445
 
        #   no_c,dict   1.95s   18.0s
446
 
        # Note that a cache of 10k nodes is more than sufficient to hold all of
447
 
        # the inventory for the last 100 revs for bzr, but not for mysql (20k
448
 
        # is enough for mysql, which saves the same 2s as using a dict)
449
 
 
450
 
        # Breakdown of mysql using time.clock()
451
 
        #   4.1s    2 calls to element.get for file_id, revision_id
452
 
        #   4.5s    cache_hit lookup
453
 
        #   7.1s    InventoryFile.copy()
454
 
        #   2.4s    InventoryDirectory.copy()
455
 
        #   0.4s    decoding unique entries
456
 
        #   1.6s    decoding entries after FIFO fills up
457
 
        #   0.8s    Adding nodes to FIFO (including flushes)
458
 
        #   0.1s    cache miss lookups
459
 
        # Using an LRU cache
460
 
        #   4.1s    2 calls to element.get for file_id, revision_id
461
 
        #   9.9s    cache_hit lookup
462
 
        #   10.8s   InventoryEntry.copy()
463
 
        #   0.3s    cache miss lookus
464
 
        #   1.2s    decoding entries
465
 
        #   1.0s    adding nodes to LRU
466
 
        if entry_cache is not None and revision is not None:
467
 
            key = (file_id, revision)
468
 
            try:
469
 
                # We copy it, because some operations may mutate it
470
 
                cached_ie = entry_cache[key]
471
 
            except KeyError:
472
 
                pass
473
 
            else:
474
 
                # Only copying directory entries drops us 2.85s => 2.35s
475
 
                if return_from_cache:
476
 
                    if cached_ie.kind == 'directory':
477
 
                        return cached_ie.copy()
478
 
                    return cached_ie
479
 
                return cached_ie.copy()
480
 
 
481
 
        kind = elt.tag
482
 
        if not InventoryEntry.versionable_kind(kind):
483
 
            raise AssertionError('unsupported entry kind %s' % kind)
484
 
 
485
 
        get_cached = _get_utf8_or_ascii
486
 
 
487
 
        file_id = get_cached(file_id)
488
 
        if revision is not None:
489
 
            revision = get_cached(revision)
490
 
        parent_id = elt_get('parent_id')
491
 
        if parent_id is not None:
492
 
            parent_id = get_cached(parent_id)
493
 
 
494
 
        if kind == 'directory':
495
 
            ie = inventory.InventoryDirectory(file_id,
496
 
                                              elt_get('name'),
497
 
                                              parent_id)
498
 
        elif kind == 'file':
499
 
            ie = inventory.InventoryFile(file_id,
500
 
                                         elt_get('name'),
501
 
                                         parent_id)
502
 
            ie.text_sha1 = elt_get('text_sha1')
503
 
            if elt_get('executable') == 'yes':
504
 
                ie.executable = True
505
 
            v = elt_get('text_size')
506
 
            ie.text_size = v and int(v)
507
 
        elif kind == 'symlink':
508
 
            ie = inventory.InventoryLink(file_id,
509
 
                                         elt_get('name'),
510
 
                                         parent_id)
511
 
            ie.symlink_target = elt_get('symlink_target')
512
 
        else:
513
 
            raise errors.UnsupportedInventoryKind(kind)
514
 
        ie.revision = revision
515
 
        if revision is not None and entry_cache is not None:
516
 
            # We cache a copy() because callers like to mutate objects, and
517
 
            # that would cause the item in cache to mutate as well.
518
 
            # This has a small effect on many-inventory performance, because
519
 
            # the majority fraction is spent in cache hits, not misses.
520
 
            entry_cache[key] = ie.copy()
521
 
 
522
 
        return ie
523
 
 
524
 
    def _unpack_revision(self, elt):
525
 
        """XML Element -> Revision object"""
526
 
        format = elt.get('format')
527
 
        format_num = self.format_num
528
 
        if self.revision_format_num is not None:
529
 
            format_num = self.revision_format_num
530
 
        if format is not None:
531
 
            if format != format_num:
532
 
                raise BzrError("invalid format version %r on revision"
533
 
                                % format)
534
 
        get_cached = _get_utf8_or_ascii
535
 
        rev = Revision(committer = elt.get('committer'),
536
 
                       timestamp = float(elt.get('timestamp')),
537
 
                       revision_id = get_cached(elt.get('revision_id')),
538
 
                       inventory_sha1 = elt.get('inventory_sha1')
539
 
                       )
540
 
        parents = elt.find('parents') or []
541
 
        for p in parents:
542
 
            rev.parent_ids.append(get_cached(p.get('revision_id')))
543
 
        self._unpack_revision_properties(elt, rev)
544
 
        v = elt.get('timezone')
545
 
        if v is None:
546
 
            rev.timezone = 0
547
 
        else:
548
 
            rev.timezone = int(v)
549
 
        rev.message = elt.findtext('message') # text of <message>
550
 
        return rev
551
 
 
552
 
    def _unpack_revision_properties(self, elt, rev):
553
 
        """Unpack properties onto a revision."""
554
 
        props_elt = elt.find('properties')
555
 
        if not props_elt:
556
 
            return
557
 
        for prop_elt in props_elt:
558
 
            if prop_elt.tag != 'property':
559
 
                raise AssertionError(
560
 
                    "bad tag under properties list: %r" % prop_elt.tag)
561
 
            name = prop_elt.get('name')
562
 
            value = prop_elt.text
563
 
            # If a property had an empty value ('') cElementTree reads
564
 
            # that back as None, convert it back to '', so that all
565
 
            # properties have string values
566
 
            if value is None:
567
 
                value = ''
568
 
            if name in rev.properties:
569
 
                raise AssertionError("repeated property %r" % name)
570
 
            rev.properties[name] = value
571
 
 
572
 
    def _find_text_key_references(self, line_iterator):
573
 
        """Core routine for extracting references to texts from inventories.
574
 
 
575
 
        This performs the translation of xml lines to revision ids.
576
 
 
577
 
        :param line_iterator: An iterator of lines, origin_version_id
578
 
        :return: A dictionary mapping text keys ((fileid, revision_id) tuples)
579
 
            to whether they were referred to by the inventory of the
580
 
            revision_id that they contain. Note that if that revision_id was
581
 
            not part of the line_iterator's output then False will be given -
582
 
            even though it may actually refer to that key.
583
 
        """
584
 
        if not self.support_altered_by_hack:
585
 
            raise AssertionError(
586
 
                "_find_text_key_references only "
587
 
                "supported for branches which store inventory as unnested xml"
588
 
                ", not on %r" % self)
589
 
        result = {}
590
 
 
591
 
        # this code needs to read every new line in every inventory for the
592
 
        # inventories [revision_ids]. Seeing a line twice is ok. Seeing a line
593
 
        # not present in one of those inventories is unnecessary but not
594
 
        # harmful because we are filtering by the revision id marker in the
595
 
        # inventory lines : we only select file ids altered in one of those
596
 
        # revisions. We don't need to see all lines in the inventory because
597
 
        # only those added in an inventory in rev X can contain a revision=X
598
 
        # line.
599
 
        unescape_revid_cache = {}
600
 
        unescape_fileid_cache = {}
601
 
 
602
 
        # jam 20061218 In a big fetch, this handles hundreds of thousands
603
 
        # of lines, so it has had a lot of inlining and optimizing done.
604
 
        # Sorry that it is a little bit messy.
605
 
        # Move several functions to be local variables, since this is a long
606
 
        # running loop.
607
 
        search = self._file_ids_altered_regex.search
608
 
        unescape = _unescape_xml
609
 
        setdefault = result.setdefault
610
 
        for line, line_key in line_iterator:
611
 
            match = search(line)
612
 
            if match is None:
613
 
                continue
614
 
            # One call to match.group() returning multiple items is quite a
615
 
            # bit faster than 2 calls to match.group() each returning 1
616
 
            file_id, revision_id = match.group('file_id', 'revision_id')
617
 
 
618
 
            # Inlining the cache lookups helps a lot when you make 170,000
619
 
            # lines and 350k ids, versus 8.4 unique ids.
620
 
            # Using a cache helps in 2 ways:
621
 
            #   1) Avoids unnecessary decoding calls
622
 
            #   2) Re-uses cached strings, which helps in future set and
623
 
            #      equality checks.
624
 
            # (2) is enough that removing encoding entirely along with
625
 
            # the cache (so we are using plain strings) results in no
626
 
            # performance improvement.
627
 
            try:
628
 
                revision_id = unescape_revid_cache[revision_id]
629
 
            except KeyError:
630
 
                unescaped = unescape(revision_id)
631
 
                unescape_revid_cache[revision_id] = unescaped
632
 
                revision_id = unescaped
633
 
 
634
 
            # Note that unconditionally unescaping means that we deserialise
635
 
            # every fileid, which for general 'pull' is not great, but we don't
636
 
            # really want to have some many fulltexts that this matters anyway.
637
 
            # RBC 20071114.
638
 
            try:
639
 
                file_id = unescape_fileid_cache[file_id]
640
 
            except KeyError:
641
 
                unescaped = unescape(file_id)
642
 
                unescape_fileid_cache[file_id] = unescaped
643
 
                file_id = unescaped
644
 
 
645
 
            key = (file_id, revision_id)
646
 
            setdefault(key, False)
647
 
            if revision_id == line_key[-1]:
648
 
                result[key] = True
649
 
        return result
650
 
 
651
 
 
652
 
serializer_v8 = Serializer_v8()