aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/jb2bz.py
blob: 55cb056b5c2e7668a7e901eb8da6802d9472e56e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
#!/usr/local/bin/python
# -*- mode: python -*-

"""
jb2bz.py - a nonce script to import bugs from JitterBug to Bugzilla
Written by Tom Emerson, tree@basistech.com

This script is provided in the hopes that it will be useful.  No
rights reserved. No guarantees expressed or implied. Use at your own
risk. May be dangerous if swallowed. If it doesn't work for you, don't
blame me. It did what I needed it to do.

This code requires a recent version of Andy Dustman's MySQLdb interface,

    http://sourceforge.net/projects/mysql-python

Share and enjoy.
"""

import rfc822, mimetools, multifile, mimetypes
import sys, re, glob, StringIO, os, stat, time
import MySQLdb, getopt

# mimetypes doesn't include everything we might encounter, yet.
if not mimetypes.types_map.has_key('.doc'):
    mimetypes.types_map['.doc'] = 'application/msword'

if not mimetypes.encodings_map.has_key('.bz2'):
    mimetypes.encodings_map['.bz2'] = "bzip2"

bug_status='CONFIRMED'
component="default"
version=""
product="" # this is required, the rest of these are defaulted as above

"""
Each bug in JitterBug is stored as a text file named by the bug number.
Additions to the bug are indicated by suffixes to this:

<bug>
<bug>.followup.*
<bug>.reply.*
<bug>.notes

The dates on the files represent the respective dates they were created/added.

All <bug>s and <bug>.reply.*s include RFC 822 mail headers. These could include
MIME file attachments as well that would need to be extracted.

There are other additions to the file names, such as

<bug>.notify

which are ignored.

Bugs in JitterBug are organized into directories. At Basis we used the following
naming conventions:

<product>-bugs         Open bugs
<product>-requests     Open Feature Requests
<product>-resolved     Bugs/Features marked fixed by engineering, but not verified
<product>-verified     Resolved defects that have been verified by QA

where <product> is either:

<product-name>

or

<product-name>-<version>
"""

def process_notes_file(current, fname):
    try:
        new_note = {}
        notes = open(fname, "r")
        s = os.fstat(notes.fileno())

        new_note['text']  = notes.read()
        new_note['timestamp'] = time.gmtime(s[stat.ST_MTIME])

        notes.close()

        current['notes'].append(new_note)

    except IOError:
        pass

def process_reply_file(current, fname):
    new_note = {}
    reply = open(fname, "r")
    msg = rfc822.Message(reply)
    new_note['text'] = "%s\n%s" % (msg['From'], msg.fp.read())
    new_note['timestamp'] = rfc822.parsedate_tz(msg['Date'])
    current["notes"].append(new_note)

def add_notes(current):
    """Add any notes that have been recorded for the current bug."""
    process_notes_file(current, "%d.notes" % current['number'])

    for f in glob.glob("%d.reply.*" % current['number']):
        process_reply_file(current, f)

    for f in glob.glob("%d.followup.*" % current['number']):
        process_reply_file(current, f)

def maybe_add_attachment(current, file, submsg):
    """Adds the attachment to the current record"""
    cd = submsg["Content-Disposition"]
    m = re.search(r'filename="([^"]+)"', cd)
    if m == None:
        return
    attachment_filename = m.group(1)
    if (submsg.gettype() == 'application/octet-stream'):
        # try get a more specific content-type for this attachment
        type, encoding = mimetypes.guess_type(m.group(1))
        if type == None:
            type = submsg.gettype()
    else:
        type = submsg.gettype()

    try:
        data = StringIO.StringIO()
        mimetools.decode(file, data, submsg.getencoding())
    except:
        return

    current['attachments'].append( ( attachment_filename, type, data.getvalue() ) )

def process_mime_body(current, file, submsg):
    data = StringIO.StringIO()
    mimetools.decode(file, data, submsg.getencoding())
    current['description'] = data.getvalue()



def process_text_plain(msg, current):
    print "Processing: %d" % current['number']
    current['description'] = msg.fp.read()

def process_multi_part(file, msg, current):
    print "Processing: %d" % current['number']
    mf = multifile.MultiFile(file)
    mf.push(msg.getparam("boundary"))
    while mf.next():
        submsg = mimetools.Message(file)
        if submsg.has_key("Content-Disposition"):
            maybe_add_attachment(current, mf, submsg)
        else:
            # This is the message body itself (always?), so process
            # accordingly
            process_mime_body(current, mf, submsg)

def process_jitterbug(filename):
    current = {}
    current['number'] = int(filename)
    current['notes'] = []
    current['attachments'] = []
    current['description'] = ''
    current['date-reported'] = ()
    current['short-description'] = ''
    
    file = open(filename, "r")
    msg = mimetools.Message(file)

    msgtype = msg.gettype()

    add_notes(current)
    current['date-reported'] = rfc822.parsedate_tz(msg['Date'])
    current['short-description'] = msg['Subject']

    if msgtype[:5] == 'text/':
        process_text_plain(msg, current)
    elif msgtype[:10] == "multipart/":
        process_multi_part(file, msg, current)
    else:
        # Huh? This should never happen.
        print "Unknown content-type: %s" % msgtype
        sys.exit(1)

    # At this point we have processed the message: we have all of the notes and
    # attachments stored, so it's time to add things to the database.
    # The schema for JitterBug 2.14 can be found at:
    #
    #    http://www.trilobyte.net/barnsons/html/dbschema.html
    #
    # The following fields need to be provided by the user:
    #
    # bug_status
    # product
    # version
    # reporter
    # component
    # resolution

    # change this to the user_id of the Bugzilla user who is blessed with the
    # imported defects
    reporter=6

    # the resolution will need to be set manually
    resolution=""

    db = MySQLdb.connect(db='bugs',user='root',host='localhost')
    cursor = db.cursor()

    cursor.execute( "INSERT INTO bugs SET " \
                    "bug_id=%s," \
                    "bug_severity='normal',"  \
                    "bug_status=%s," \
                    "creation_ts=%s,"  \
                    "delta_ts=%s,"  \
                    "short_desc=%s," \
                    "product=%s," \
                    "rep_platform='All'," \
                    "assigned_to=%s,"
                    "reporter=%s," \
                    "version=%s,"  \
                    "component=%s,"  \
                    "resolution=%s",
                    [ current['number'],
                      bug_status,
                      time.strftime("%Y-%m-%d %H:%M:%S", current['date-reported'][:9]),
                      time.strftime("%Y-%m-%d %H:%M:%S", current['date-reported'][:9]),
                      current['short-description'],
                      product,
                      reporter,
                      reporter,
                      version,
                      component,
                      resolution] )

    # This is the initial long description associated with the bug report
    cursor.execute( "INSERT INTO longdescs VALUES (%s,%s,%s,%s)",
                    [ current['number'],
                      reporter,
                      time.strftime("%Y-%m-%d %H:%M:%S", current['date-reported'][:9]),
                      current['description'] ] )

    # Add whatever notes are associated with this defect
    for n in current['notes']:
        cursor.execute( "INSERT INTO longdescs VALUES (%s,%s,%s,%s)",
                        [current['number'],
                         reporter,
                         time.strftime("%Y-%m-%d %H:%M:%S", n['timestamp'][:9]),
                         n['text']])

    # add attachments associated with this defect
    for a in current['attachments']:
        cursor.execute( "INSERT INTO attachments SET " \
                        "bug_id=%s, creation_ts=%s, description='', mimetype=%s," \
                        "filename=%s, submitter_id=%s",
                        [ current['number'],
                          time.strftime("%Y-%m-%d %H:%M:%S", current['date-reported'][:9]),
                          a[1], a[0], reporter ])
        cursor.execute( "INSERT INTO attach_data SET " \
                        "id=LAST_INSERT_ID(), thedata=%s",
                        [ a[2] ])

    cursor.close()
    db.close()

def usage():
    print """Usage: jb2bz.py [OPTIONS] Product

Where OPTIONS are one or more of the following:

  -h                This help information.
  -s STATUS         One of UNCONFIRMED, CONFIRMED, IN_PROGRESS, RESOLVED, VERIFIED
                    (default is CONFIRMED)
  -c COMPONENT      The component to attach to each bug as it is important. This should be
                    valid component for the Product.
  -v VERSION        Version to assign to these defects.

Product is the Product to assign these defects to.

All of the JitterBugs in the current directory are imported, including replies, notes,
attachments, and similar noise.
"""
    sys.exit(1)


def main():
    global bug_status, component, version, product
    opts, args = getopt.getopt(sys.argv[1:], "hs:c:v:")

    for o,a in opts:
        if o == "-s":
            if a in ('UNCONFIRMED','CONFIRMED','IN_PROGRESS','RESOLVED','VERIFIED'):
                bug_status = a
        elif o == '-c':
            component = a
        elif o == '-v':
            version = a
        elif o == '-h':
            usage()

    if len(args) != 1:
        sys.stderr.write("Must specify the Product.\n")
        sys.exit(1)

    product = args[0]

    for bug in filter(lambda x: re.match(r"\d+$", x), glob.glob("*")):
        process_jitterbug(bug)
        

if __name__ == "__main__":
    main()