~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/xml8.py

(jameinel) Allow 'bzr serve' to interpret SIGHUP as a graceful shutdown.
 (bug #795025) (John A Meinel)

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006, 2007, 2008 Canonical Ltd
 
1
# Copyright (C) 2005-2010 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
12
12
#
13
13
# You should have received a copy of the GNU General Public License
14
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
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
16
 
17
17
import cStringIO
18
18
import re
21
21
    cache_utf8,
22
22
    errors,
23
23
    inventory,
 
24
    lazy_regex,
24
25
    revision as _mod_revision,
25
26
    trace,
26
27
    )
27
 
from bzrlib.xml_serializer import SubElement, Element, Serializer
28
 
from bzrlib.inventory import ROOT_ID, Inventory, InventoryEntry
 
28
from bzrlib.xml_serializer import (
 
29
    Element,
 
30
    SubElement,
 
31
    XMLSerializer,
 
32
    escape_invalid_chars,
 
33
    )
 
34
from bzrlib.inventory import InventoryEntry
29
35
from bzrlib.revision import Revision
30
36
from bzrlib.errors import BzrError
31
37
 
40
46
    ">":">",
41
47
    }
42
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
 
43
78
 
44
79
def _ensure_utf8_re():
45
80
    """Make sure the _utf8_re and _unicode_re regexes have been compiled."""
92
127
    # to check if None, rather than try/KeyError
93
128
    text = _map.get(unicode_or_utf8_str)
94
129
    if text is None:
95
 
        if unicode_or_utf8_str.__class__ == unicode:
 
130
        if unicode_or_utf8_str.__class__ is unicode:
96
131
            # The alternative policy is to do a regular UTF8 encoding
97
132
            # and then escape only XML meta characters.
98
133
            # Performance is equivalent once you use cache_utf8. *However*
128
163
    # This is fairly optimized because we know what cElementTree does, this is
129
164
    # not meant as a generic function for all cases. Because it is possible for
130
165
    # an 8-bit string to not be ascii or valid utf8.
131
 
    if a_str.__class__ == unicode:
 
166
    if a_str.__class__ is unicode:
132
167
        return _encode_utf8(a_str)
133
168
    else:
134
 
        return _get_cached_ascii(a_str)
 
169
        return intern(a_str)
135
170
 
136
171
 
137
172
def _clear_cache():
139
174
    _to_escaped_map.clear()
140
175
 
141
176
 
142
 
class Serializer_v8(Serializer):
 
177
class Serializer_v8(XMLSerializer):
143
178
    """This serialiser adds rich roots.
144
179
 
145
180
    Its revision format number matches its inventory number.
156
191
    format_num = '8'
157
192
    revision_format_num = None
158
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
 
159
201
    def _check_revisions(self, inv):
160
202
        """Extension point for subclasses to check during serialisation.
161
203
 
162
204
        :param inv: An inventory about to be serialised, to be checked.
163
 
        :raises: AssertionError if an error has occured.
 
205
        :raises: AssertionError if an error has occurred.
164
206
        """
165
207
        if inv.revision_id is None:
166
 
            raise AssertionError()
 
208
            raise AssertionError("inv.revision_id is None")
167
209
        if inv.root.revision is None:
168
 
            raise AssertionError()
 
210
            raise AssertionError("inv.root.revision is None")
169
211
 
170
212
    def _check_cache_size(self, inv_size, entry_cache):
171
213
        """Check that the entry_cache is large enough.
211
253
 
212
254
    def write_inventory(self, inv, f, working=False):
213
255
        """Write inventory to a file.
214
 
        
 
256
 
215
257
        :param inv: the inventory to write.
216
258
        :param f: the file to write. (May be None if the lines are the desired
217
259
            output).
341
383
            root.set('timezone', str(rev.timezone))
342
384
        root.text = '\n'
343
385
        msg = SubElement(root, 'message')
344
 
        msg.text = rev.message
 
386
        msg.text = escape_invalid_chars(rev.message)[0]
345
387
        msg.tail = '\n'
346
388
        if rev.parent_ids:
347
389
            pelts = SubElement(root, 'parents')
366
408
            prop_elt.tail = '\n'
367
409
        top_elt.tail = '\n'
368
410
 
369
 
    def _unpack_inventory(self, elt, revision_id=None, entry_cache=None):
 
411
    def _unpack_inventory(self, elt, revision_id=None, entry_cache=None,
 
412
                          return_from_cache=False):
370
413
        """Construct from XML Element"""
371
414
        if elt.tag != 'inventory':
372
415
            raise errors.UnexpectedInventoryFormat('Root tag is %r' % elt.tag)
379
422
            revision_id = cache_utf8.encode(revision_id)
380
423
        inv = inventory.Inventory(root_id=None, revision_id=revision_id)
381
424
        for e in elt:
382
 
            ie = self._unpack_entry(e, entry_cache=entry_cache)
 
425
            ie = self._unpack_entry(e, entry_cache=entry_cache,
 
426
                                    return_from_cache=return_from_cache)
383
427
            inv.add(ie)
384
428
        self._check_cache_size(len(inv), entry_cache)
385
429
        return inv
386
430
 
387
 
    def _unpack_entry(self, elt, entry_cache=None):
 
431
    def _unpack_entry(self, elt, entry_cache=None, return_from_cache=False):
388
432
        elt_get = elt.get
389
433
        file_id = elt_get('file_id')
390
434
        revision = elt_get('revision')
422
466
        if entry_cache is not None and revision is not None:
423
467
            key = (file_id, revision)
424
468
            try:
425
 
                # We copy it, because some operatations may mutate it
 
469
                # We copy it, because some operations may mutate it
426
470
                cached_ie = entry_cache[key]
427
471
            except KeyError:
428
472
                pass
429
473
            else:
430
474
                # Only copying directory entries drops us 2.85s => 2.35s
431
 
                # if cached_ie.kind == 'directory':
432
 
                #     return cached_ie.copy()
433
 
                # return cached_ie
 
475
                if return_from_cache:
 
476
                    if cached_ie.kind == 'directory':
 
477
                        return cached_ie.copy()
 
478
                    return cached_ie
434
479
                return cached_ie.copy()
435
480
 
436
481
        kind = elt.tag
524
569
                raise AssertionError("repeated property %r" % name)
525
570
            rev.properties[name] = value
526
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
 
527
651
 
528
652
serializer_v8 = Serializer_v8()