~launchpad-pqm/launchpad/devel

11799.4.1 by Gavin Panella
Script to alter/repair bug import XML from the Openfiler project. May be generally useful.
1
#!/usr/bin/env python2.6
11799.4.3 by Gavin Panella
Allow specifying the project.
2
# -*- mode: python -*-
11799.4.1 by Gavin Panella
Script to alter/repair bug import XML from the Openfiler project. May be generally useful.
3
4
from base64 import standard_b64encode
11799.4.5 by Gavin Panella
Add an option parser and accept some nickname-related options.
5
from optparse import OptionParser
11799.4.6 by Gavin Panella
Move the __main__ content into a main() function.
6
import sys
11799.4.7 by Gavin Panella
Create better reporting functions.
7
11799.4.1 by Gavin Panella
Script to alter/repair bug import XML from the Openfiler project. May be generally useful.
8
from lxml import etree
9
10
11
NS = "https://launchpad.net/xmlns/2006/bugs"
12
13
14
def norm_text(elem):
15
    if elem is not None:
16
        if elem.text is None:
17
            elem.text = u""
18
        else:
19
            elem.text = elem.text.strip()
20
21
22
def truncate(text, message=None):
23
    lines = text.splitlines()
24
    if len(lines) >= 30:
25
        if message is None:
26
            message = "[Truncated]"
27
        else:
28
            message = "[Truncated; %s]" % message
29
        return u"%s...\n\n%s" % (
30
            "\n".join(lines[:30]).strip(), message)
31
    else:
32
        return text
33
34
11799.4.7 by Gavin Panella
Create better reporting functions.
35
def problem(message):
36
    sys.stderr.write("{0}\n".format(message))
37
38
39
def problem_detail(message):
40
    sys.stderr.write("  {0}\n".format(message))
41
42
43
def problem_resolution(message):
44
    sys.stderr.write("  --> {0}\n".format(message))
45
46
47
def problem_resolved():
48
    sys.stderr.write("\n")
49
50
11799.4.9 by Gavin Panella
Rename munge() to massage().
51
def massage(root, project_name, fix_nickname, tag_nickname):
11799.4.4 by Gavin Panella
Move the munging code into a function.
52
    """Fix problems in the bug import XML tree.
53
11799.4.5 by Gavin Panella
Add an option parser and accept some nickname-related options.
54
    This includes:
11799.4.4 by Gavin Panella
Move the munging code into a function.
55
56
    - Adding a tags element if one does not exist,
57
58
    - Fixing up the bug nickname, adding the existing nickname as a tag,
59
11799.4.5 by Gavin Panella
Add an option parser and accept some nickname-related options.
60
    - Fixing up the description, including truncating it if it's too long,
11799.4.4 by Gavin Panella
Move the munging code into a function.
61
11799.4.5 by Gavin Panella
Add an option parser and accept some nickname-related options.
62
    - Fixing up the first comment, including truncating it if it's too long,
11799.4.4 by Gavin Panella
Move the munging code into a function.
63
64
    - Normalizing whitespace.
65
66
    """
11799.4.1 by Gavin Panella
Script to alter/repair bug import XML from the Openfiler project. May be generally useful.
67
    # Scan the tree, fixing up issues.
68
    for bug in root.findall('{%s}bug' % NS):
69
        # Get or create the tags element.
70
        tags = bug.find('{%s}tags' % NS)
71
        if tags is None:
72
            tags = etree.SubElement(bug, '{%s}tags' % NS)
73
74
        nickname = bug.find('{%s}nickname' % NS)
75
        if nickname is None:
11799.4.5 by Gavin Panella
Add an option parser and accept some nickname-related options.
76
            # Add an empty nickname to be filled in later.
11799.4.1 by Gavin Panella
Script to alter/repair bug import XML from the Openfiler project. May be generally useful.
77
            nickname = etree.SubElement(bug, '{%s}nickname' % NS)
11799.4.5 by Gavin Panella
Add an option parser and accept some nickname-related options.
78
        elif tag_nickname:
79
            # Add the original nickname as a tag.
11799.4.1 by Gavin Panella
Script to alter/repair bug import XML from the Openfiler project. May be generally useful.
80
            etree.SubElement(tags, '{%s}tag' % NS).text = nickname.text
81
82
        # Change the nickname.
11799.4.5 by Gavin Panella
Add an option parser and accept some nickname-related options.
83
        if nickname.text is None or fix_nickname:
84
            nickname.text = u"%s-%s" % (project_name, bug.get('id'))
11799.4.1 by Gavin Panella
Script to alter/repair bug import XML from the Openfiler project. May be generally useful.
85
86
        # Get the first comment and its text. We'll need these later.
87
        first_comment = bug.find('{%s}comment' % NS)
88
        first_comment_text = first_comment.find('{%s}text' % NS)
89
        norm_text(first_comment_text)
90
91
        # Check the description.
92
        description = bug.find('{%s}description' % NS)
93
        norm_text(description)
94
        if len(description.text) == 0:
11799.4.7 by Gavin Panella
Create better reporting functions.
95
            problem("Bug %s has no description." % bug.get('id'))
11799.4.1 by Gavin Panella
Script to alter/repair bug import XML from the Openfiler project. May be generally useful.
96
            # Try and get the description from the first comment.
97
            if first_comment_text is None:
11799.4.7 by Gavin Panella
Create better reporting functions.
98
                problem_detail("No comments!")
99
                problem_resolution("Setting description to '-'.")
11799.4.1 by Gavin Panella
Script to alter/repair bug import XML from the Openfiler project. May be generally useful.
100
                description.text = u'-'
101
            elif len(first_comment_text.text) == 0:
11799.4.7 by Gavin Panella
Create better reporting functions.
102
                problem_detail("First comment has no text!")
103
                problem_resolution("Setting description to '-'.")
11799.4.1 by Gavin Panella
Script to alter/repair bug import XML from the Openfiler project. May be generally useful.
104
                description.text = u'-'
105
            else:
11799.4.7 by Gavin Panella
Create better reporting functions.
106
                problem_detail("First comment has text.")
107
                problem_resolution("Removing description.")
11799.4.10 by Gavin Panella
Fix comment.
108
                # The spec says that the description is not optional, but the
109
                # importer treats it as optional.
11799.4.1 by Gavin Panella
Script to alter/repair bug import XML from the Openfiler project. May be generally useful.
110
                bug.remove(description)
11799.4.7 by Gavin Panella
Create better reporting functions.
111
            problem_resolved()
11799.4.1 by Gavin Panella
Script to alter/repair bug import XML from the Openfiler project. May be generally useful.
112
        elif len(description.text) > 50000:
11799.4.7 by Gavin Panella
Create better reporting functions.
113
            problem(
114
                "Bug %s's description is too long (%d chars)." % (
11799.4.1 by Gavin Panella
Script to alter/repair bug import XML from the Openfiler project. May be generally useful.
115
                    bug.get('id'), len(description.text),))
116
            # Compare the description to the first comment. If it's
117
            # the same, we don't need the description.
118
            if first_comment_text is None:
11799.4.7 by Gavin Panella
Create better reporting functions.
119
                problem_detail("No comments!")
120
                problem_resolution("Adding comment.")
11799.4.1 by Gavin Panella
Script to alter/repair bug import XML from the Openfiler project. May be generally useful.
121
                raise NotImplementedError("Add a comment.")
122
            elif description.text == first_comment_text.text:
11799.4.7 by Gavin Panella
Create better reporting functions.
123
                problem_detail('Description is same as first comment.')
124
                problem_resolution('Trimming description.')
11799.4.1 by Gavin Panella
Script to alter/repair bug import XML from the Openfiler project. May be generally useful.
125
                # It's safe to point the user to an attachment here,
126
                # even though it has not yet been created. It will be
127
                # created later because the first comment is also too
128
                # long.
129
                description.text = truncate(
130
                    description.text, 'see "Full description" attachment')
131
            else:
11799.4.7 by Gavin Panella
Create better reporting functions.
132
                problem_resolution("Truncating description.")
11799.4.1 by Gavin Panella
Script to alter/repair bug import XML from the Openfiler project. May be generally useful.
133
                raise NotImplementedError("Fix overlong description.")
11799.4.7 by Gavin Panella
Create better reporting functions.
134
            problem_resolved()
11799.4.1 by Gavin Panella
Script to alter/repair bug import XML from the Openfiler project. May be generally useful.
135
136
        # Check first comment text.
137
        if first_comment_text is not None:
138
            if len(first_comment_text.text) == 0:
11799.4.7 by Gavin Panella
Create better reporting functions.
139
                problem(
140
                    "Bug %s's first comment has no text." % bug.get('id'))
141
                problem_resolution("Setting comment text to '-'.")
11799.4.1 by Gavin Panella
Script to alter/repair bug import XML from the Openfiler project. May be generally useful.
142
                first_comment_text.text = u'-'
11799.4.7 by Gavin Panella
Create better reporting functions.
143
                problem_resolved()
11799.4.1 by Gavin Panella
Script to alter/repair bug import XML from the Openfiler project. May be generally useful.
144
            elif len(first_comment_text.text) > 50000:
11799.4.7 by Gavin Panella
Create better reporting functions.
145
                problem(
146
                    "Bug %s's first comment is too long (%d chars)." % (
11799.4.1 by Gavin Panella
Script to alter/repair bug import XML from the Openfiler project. May be generally useful.
147
                        bug.get('id'), len(first_comment_text.text)))
148
                # Save the original text as an attachment.
11799.4.7 by Gavin Panella
Create better reporting functions.
149
                problem_resolution('Adding attachment.')
11799.4.1 by Gavin Panella
Script to alter/repair bug import XML from the Openfiler project. May be generally useful.
150
                attachment = etree.SubElement(
151
                    first_comment, '{%s}attachment' % NS)
152
                etree.SubElement(attachment, '{%s}filename' % NS).text = (
11799.4.3 by Gavin Panella
Allow specifying the project.
153
                    u"%s-bug-%s-full-description.txt" % (
154
                        project_name, bug.get('id')))
11799.4.1 by Gavin Panella
Script to alter/repair bug import XML from the Openfiler project. May be generally useful.
155
                etree.SubElement(attachment, '{%s}title' % NS).text = (
156
                    u"Full description (text/plain, utf-8)")
157
                etree.SubElement(attachment, '{%s}mimetype' % NS).text = (
158
                    u"text/plain")
159
                etree.SubElement(attachment, '{%s}contents' % NS).text = (
160
                    standard_b64encode(
161
                        first_comment_text.text.encode('utf-8')))
162
                # Trim the comment text.
11799.4.7 by Gavin Panella
Create better reporting functions.
163
                problem_resolution('Trimming comment text.')
11799.4.1 by Gavin Panella
Script to alter/repair bug import XML from the Openfiler project. May be generally useful.
164
                first_comment_text.text = truncate(
165
                    first_comment_text.text,
166
                    'see "Full description" attachment')
11799.4.7 by Gavin Panella
Create better reporting functions.
167
                problem_resolved()
11799.4.1 by Gavin Panella
Script to alter/repair bug import XML from the Openfiler project. May be generally useful.
168
11799.4.4 by Gavin Panella
Move the munging code into a function.
169
11799.4.6 by Gavin Panella
Move the __main__ content into a main() function.
170
def main(arguments):
11799.4.5 by Gavin Panella
Add an option parser and accept some nickname-related options.
171
    # optparse.OptionParser uses lower-case for usage and help text by
172
    # default. This is distressing, so it is corrected for below.
173
    usage = "Usage: %prog [options]"
174
    description = """
175
        This acts as a filter: pipe bug import XML into stdin and capture
176
        stdout. By default it will ensure that bug descriptions and the first
177
        comment are correct. If either exceeds 50,000 characters it is
178
        truncated and an attachment is created to hold the original.
179
        """
180
    parser = OptionParser(
181
        usage=usage,
182
        description=description.strip(),
183
        add_help_option=False)
184
    parser.add_option(
185
        "-p", "--project", dest="project_name", metavar="NAME",
186
        help="The project to which this import data refers.")
187
    parser.add_option(
188
        "--fix-nickname", action="store_true", dest="fix_nickname",
189
        help="Normalize the nickname to ${project_name}-${bug-id}.")
190
    parser.add_option(
191
        "--tag-nickname", action="store_true", dest="tag_nickname",
192
        help="Add the original bug nickname as a tag.")
193
    parser.add_option(
194
        "-h", "--help", action="help",
195
        help="Show this help message and exit.")
196
    parser.set_defaults(
197
        project_name=None,
198
        fix_nickname=False,
199
        tag_nickname=False)
200
11799.4.6 by Gavin Panella
Move the __main__ content into a main() function.
201
    options, args = parser.parse_args(arguments)
11799.4.5 by Gavin Panella
Add an option parser and accept some nickname-related options.
202
    if len(args) != 0:
203
        parser.error("Positional arguments are not recognized.")
204
    if options.project_name is None:
205
        parser.error("A project name must be specified.")
206
11799.4.7 by Gavin Panella
Create better reporting functions.
207
    tree = etree.parse(sys.stdin)
11799.4.9 by Gavin Panella
Rename munge() to massage().
208
    massage(
11799.4.5 by Gavin Panella
Add an option parser and accept some nickname-related options.
209
        root=tree.getroot(),
210
        project_name=options.project_name,
211
        fix_nickname=options.fix_nickname,
212
        tag_nickname=options.tag_nickname)
213
    tree.write(
11799.4.7 by Gavin Panella
Create better reporting functions.
214
        sys.stdout, encoding='utf-8',
11799.4.5 by Gavin Panella
Add an option parser and accept some nickname-related options.
215
        pretty_print=True, xml_declaration=True)
11799.4.6 by Gavin Panella
Move the __main__ content into a main() function.
216
217
    return 0
218
219
220
if __name__ == '__main__':
221
    sys.exit(main(sys.argv[1:]))