summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--contrib/jb2bz.py303
1 files changed, 303 insertions, 0 deletions
diff --git a/contrib/jb2bz.py b/contrib/jb2bz.py
new file mode 100644
index 000000000..dbd47ea7a
--- /dev/null
+++ b/contrib/jb2bz.py
@@ -0,0 +1,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()