jb2bz.py 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. #!/usr/local/bin/python
  2. # -*- mode: python -*-
  3. """
  4. jb2bz.py - a nonce script to import bugs from JitterBug to Bugzilla
  5. Written by Tom Emerson, tree@basistech.com
  6. This script is provided in the hopes that it will be useful. No
  7. rights reserved. No guarantees expressed or implied. Use at your own
  8. risk. May be dangerous if swallowed. If it doesn't work for you, don't
  9. blame me. It did what I needed it to do.
  10. This code requires a recent version of Andy Dustman's MySQLdb interface,
  11. http://sourceforge.net/projects/mysql-python
  12. Share and enjoy.
  13. """
  14. import rfc822, mimetools, multifile, mimetypes
  15. import sys, re, glob, StringIO, os, stat, time
  16. import MySQLdb, getopt
  17. # mimetypes doesn't include everything we might encounter, yet.
  18. if not mimetypes.types_map.has_key('.doc'):
  19. mimetypes.types_map['.doc'] = 'application/msword'
  20. if not mimetypes.encodings_map.has_key('.bz2'):
  21. mimetypes.encodings_map['.bz2'] = "bzip2"
  22. bug_status='NEW'
  23. component="default"
  24. version=""
  25. product="" # this is required, the rest of these are defaulted as above
  26. """
  27. Each bug in JitterBug is stored as a text file named by the bug number.
  28. Additions to the bug are indicated by suffixes to this:
  29. <bug>
  30. <bug>.followup.*
  31. <bug>.reply.*
  32. <bug>.notes
  33. The dates on the files represent the respective dates they were created/added.
  34. All <bug>s and <bug>.reply.*s include RFC 822 mail headers. These could include
  35. MIME file attachments as well that would need to be extracted.
  36. There are other additions to the file names, such as
  37. <bug>.notify
  38. which are ignored.
  39. Bugs in JitterBug are organized into directories. At Basis we used the following
  40. naming conventions:
  41. <product>-bugs Open bugs
  42. <product>-requests Open Feature Requests
  43. <product>-resolved Bugs/Features marked fixed by engineering, but not verified
  44. <product>-verified Resolved defects that have been verified by QA
  45. where <product> is either:
  46. <product-name>
  47. or
  48. <product-name>-<version>
  49. """
  50. def process_notes_file(current, fname):
  51. try:
  52. new_note = {}
  53. notes = open(fname, "r")
  54. s = os.fstat(notes.fileno())
  55. new_note['text'] = notes.read()
  56. new_note['timestamp'] = time.gmtime(s[stat.ST_MTIME])
  57. notes.close()
  58. current['notes'].append(new_note)
  59. except IOError:
  60. pass
  61. def process_reply_file(current, fname):
  62. new_note = {}
  63. reply = open(fname, "r")
  64. msg = rfc822.Message(reply)
  65. new_note['text'] = "%s\n%s" % (msg['From'], msg.fp.read())
  66. new_note['timestamp'] = rfc822.parsedate_tz(msg['Date'])
  67. current["notes"].append(new_note)
  68. def add_notes(current):
  69. """Add any notes that have been recorded for the current bug."""
  70. process_notes_file(current, "%d.notes" % current['number'])
  71. for f in glob.glob("%d.reply.*" % current['number']):
  72. process_reply_file(current, f)
  73. for f in glob.glob("%d.followup.*" % current['number']):
  74. process_reply_file(current, f)
  75. def maybe_add_attachment(current, file, submsg):
  76. """Adds the attachment to the current record"""
  77. cd = submsg["Content-Disposition"]
  78. m = re.search(r'filename="([^"]+)"', cd)
  79. if m == None:
  80. return
  81. attachment_filename = m.group(1)
  82. if (submsg.gettype() == 'application/octet-stream'):
  83. # try get a more specific content-type for this attachment
  84. type, encoding = mimetypes.guess_type(m.group(1))
  85. if type == None:
  86. type = submsg.gettype()
  87. else:
  88. type = submsg.gettype()
  89. try:
  90. data = StringIO.StringIO()
  91. mimetools.decode(file, data, submsg.getencoding())
  92. except:
  93. return
  94. current['attachments'].append( ( attachment_filename, type, data.getvalue() ) )
  95. def process_mime_body(current, file, submsg):
  96. data = StringIO.StringIO()
  97. mimetools.decode(file, data, submsg.getencoding())
  98. current['description'] = data.getvalue()
  99. def process_text_plain(msg, current):
  100. print "Processing: %d" % current['number']
  101. current['description'] = msg.fp.read()
  102. def process_multi_part(file, msg, current):
  103. print "Processing: %d" % current['number']
  104. mf = multifile.MultiFile(file)
  105. mf.push(msg.getparam("boundary"))
  106. while mf.next():
  107. submsg = mimetools.Message(file)
  108. if submsg.has_key("Content-Disposition"):
  109. maybe_add_attachment(current, mf, submsg)
  110. else:
  111. # This is the message body itself (always?), so process
  112. # accordingly
  113. process_mime_body(current, mf, submsg)
  114. def process_jitterbug(filename):
  115. current = {}
  116. current['number'] = int(filename)
  117. current['notes'] = []
  118. current['attachments'] = []
  119. current['description'] = ''
  120. current['date-reported'] = ()
  121. current['short-description'] = ''
  122. file = open(filename, "r")
  123. msg = mimetools.Message(file)
  124. msgtype = msg.gettype()
  125. add_notes(current)
  126. current['date-reported'] = rfc822.parsedate_tz(msg['Date'])
  127. current['short-description'] = msg['Subject']
  128. if msgtype[:5] == 'text/':
  129. process_text_plain(msg, current)
  130. elif msgtype[:10] == "multipart/":
  131. process_multi_part(file, msg, current)
  132. else:
  133. # Huh? This should never happen.
  134. print "Unknown content-type: %s" % msgtype
  135. sys.exit(1)
  136. # At this point we have processed the message: we have all of the notes and
  137. # attachments stored, so it's time to add things to the database.
  138. # The schema for JitterBug 2.14 can be found at:
  139. #
  140. # http://www.trilobyte.net/barnsons/html/dbschema.html
  141. #
  142. # The following fields need to be provided by the user:
  143. #
  144. # bug_status
  145. # product
  146. # version
  147. # reporter
  148. # component
  149. # resolution
  150. # change this to the user_id of the Bugzilla user who is blessed with the
  151. # imported defects
  152. reporter=6
  153. # the resolution will need to be set manually
  154. resolution=""
  155. db = MySQLdb.connect(db='bugs',user='root',host='localhost')
  156. cursor = db.cursor()
  157. cursor.execute( "INSERT INTO bugs SET " \
  158. "bug_id=%s," \
  159. "bug_severity='normal'," \
  160. "bug_status=%s," \
  161. "creation_ts=%s," \
  162. "delta_ts=%s," \
  163. "short_desc=%s," \
  164. "product=%s," \
  165. "rep_platform='All'," \
  166. "assigned_to=%s,"
  167. "reporter=%s," \
  168. "version=%s," \
  169. "component=%s," \
  170. "resolution=%s",
  171. [ current['number'],
  172. bug_status,
  173. time.strftime("%Y-%m-%d %H:%M:%S", current['date-reported'][:9]),
  174. time.strftime("%Y-%m-%d %H:%M:%S", current['date-reported'][:9]),
  175. current['short-description'],
  176. product,
  177. reporter,
  178. reporter,
  179. version,
  180. component,
  181. resolution] )
  182. # This is the initial long description associated with the bug report
  183. cursor.execute( "INSERT INTO longdescs VALUES (%s,%s,%s,%s)",
  184. [ current['number'],
  185. reporter,
  186. time.strftime("%Y-%m-%d %H:%M:%S", current['date-reported'][:9]),
  187. current['description'] ] )
  188. # Add whatever notes are associated with this defect
  189. for n in current['notes']:
  190. cursor.execute( "INSERT INTO longdescs VALUES (%s,%s,%s,%s)",
  191. [current['number'],
  192. reporter,
  193. time.strftime("%Y-%m-%d %H:%M:%S", n['timestamp'][:9]),
  194. n['text']])
  195. # add attachments associated with this defect
  196. for a in current['attachments']:
  197. cursor.execute( "INSERT INTO attachments SET " \
  198. "bug_id=%s, creation_ts=%s, description='', mimetype=%s," \
  199. "filename=%s, submitter_id=%s",
  200. [ current['number'],
  201. time.strftime("%Y-%m-%d %H:%M:%S", current['date-reported'][:9]),
  202. a[1], a[0], reporter ])
  203. cursor.execute( "INSERT INTO attach_data SET " \
  204. "id=LAST_INSERT_ID(), thedata=%s",
  205. [ a[2] ])
  206. cursor.close()
  207. db.close()
  208. def usage():
  209. print """Usage: jb2bz.py [OPTIONS] Product
  210. Where OPTIONS are one or more of the following:
  211. -h This help information.
  212. -s STATUS One of UNCONFIRMED, NEW, ASSIGNED, REOPENED, RESOLVED, VERIFIED, CLOSED
  213. (default is NEW)
  214. -c COMPONENT The component to attach to each bug as it is important. This should be
  215. valid component for the Product.
  216. -v VERSION Version to assign to these defects.
  217. Product is the Product to assign these defects to.
  218. All of the JitterBugs in the current directory are imported, including replies, notes,
  219. attachments, and similar noise.
  220. """
  221. sys.exit(1)
  222. def main():
  223. global bug_status, component, version, product
  224. opts, args = getopt.getopt(sys.argv[1:], "hs:c:v:")
  225. for o,a in opts:
  226. if o == "-s":
  227. if a in ('UNCONFIRMED','NEW','ASSIGNED','REOPENED','RESOLVED','VERIFIED','CLOSED'):
  228. bug_status = a
  229. elif o == '-c':
  230. component = a
  231. elif o == '-v':
  232. version = a
  233. elif o == '-h':
  234. usage()
  235. if len(args) != 1:
  236. sys.stderr.write("Must specify the Product.\n")
  237. sys.exit(1)
  238. product = args[0]
  239. for bug in filter(lambda x: re.match(r"\d+$", x), glob.glob("*")):
  240. process_jitterbug(bug)
  241. if __name__ == "__main__":
  242. main()