jb2bz.py 9.3 KB
Newer Older
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
#!/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='NEW'
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,"  \
                    "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]),
                      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, thedata=%s, submitter_id=%s",
                        [ current['number'],
                          time.strftime("%Y-%m-%d %H:%M:%S", current['date-reported'][:9]),
                          a[1], a[0], a[2], reporter ])

    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, NEW, ASSIGNED, REOPENED, RESOLVED, VERIFIED, CLOSED
                    (default is NEW)
  -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','NEW','ASSIGNED','REOPENED','RESOLVED','VERIFIED','CLOSED'):
                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()