Software Freedom Law Center

root/trunk/trac/contrib/bugzilla2trac.py

Revision 103, 32.6 kB (checked in by bkuhn, 8 months ago)

r129@hughes: bkuhn | 2008-05-01 21:46:34 -0400

  • Merged upstream trac via: svk smerge /loblaw/local/branches/trac.upstream-r6969 .
Line 
1 #!/usr/bin/env python
2
3 """
4 Import a Bugzilla items into a Trac database.
5
6 Requires:  Trac 0.9b1 from http://trac.edgewall.org/
7            Python 2.3 from http://www.python.org/
8            MySQL >= 3.23 from http://www.mysql.org/
9
10 Thanks:    Mark Rowe <mrowe@bluewire.net.nz>
11             for original TracDatabase class
12
13 Copyright 2004, Dmitry Yusupov <dmitry_yus@yahoo.com>
14
15 Many enhancements, Bill Soudan <bill@soudan.net>
16 Other enhancements, Florent Guillaume <fg@nuxeo.com>
17 Reworked, Jeroen Ruigrok van der Werven <asmodai@in-nomine.org>
18
19 $Id$
20 """
21
22 import re
23
24 ###
25 ### Conversion Settings -- edit these before running if desired
26 ###
27
28 # Bugzilla version.  You can find this in Bugzilla's globals.pl file.
29 #
30 # Currently, the following bugzilla versions are known to work:
31 #   2.11 (2110), 2.16.5 (2165), 2.18.3 (2183), 2.19.1 (2191), 2.23.3 (2233)
32 #
33 # If you run this script on a version not listed here and it is successful,
34 # please file a ticket at http://trac.edgewall.org/ and assign it to
35 # jruigrok.
36 BZ_VERSION = 2180
37
38 # MySQL connection parameters for the Bugzilla database.  These can also
39 # be specified on the command line.
40 BZ_DB = ""
41 BZ_HOST = ""
42 BZ_USER = ""
43 BZ_PASSWORD = ""
44
45 # Path to the Trac environment.
46 TRAC_ENV = "/usr/local/trac"
47
48 # If true, all existing Trac tickets and attachments will be removed
49 # prior to import.
50 TRAC_CLEAN = True
51
52 # Enclose imported ticket description and comments in a {{{ }}}
53 # preformat block?  This formats the text in a fixed-point font.
54 PREFORMAT_COMMENTS = False
55
56 # Replace bug numbers in comments with #xyz
57 REPLACE_BUG_NO = False
58
59 # Severities
60 SEVERITIES = [
61     ("blocker",  "1"),
62     ("critical", "2"),
63     ("major",    "3"),
64     ("normal",   "4"),
65     ("minor",    "5"),
66     ("trivial",  "6")
67 ]
68
69 # Priorities
70 # If using the default Bugzilla priorities of P1 - P5, do not change anything
71 # here.
72 # If you have other priorities defined please change the P1 - P5 mapping to
73 # the order you want.  You can also collapse multiple priorities on bugzilla's
74 # side into the same priority on Trac's side, simply adjust PRIORITIES_MAP.
75 PRIORITIES = [
76     ("highest", "1"),
77     ("high",    "2"),
78     ("normal",  "3"),
79     ("low",     "4"),
80     ("lowest",  "5")
81 ]
82
83 # Bugzilla: Trac
84 # NOTE: Use lowercase.
85 PRIORITIES_MAP = {
86     "p1": "highest",
87     "p2": "high",
88     "p3": "normal",
89     "p4": "low",
90     "p5": "lowest"
91 }
92
93 # By default, all bugs are imported from Bugzilla.  If you add a list
94 # of products here, only bugs from those products will be imported.
95 PRODUCTS = []
96 # These Bugzilla products will be ignored during import.
97 IGNORE_PRODUCTS = []
98
99 # These milestones are ignored
100 IGNORE_MILESTONES = ["---"]
101
102 # These logins are converted to these user ids
103 LOGIN_MAP = {
104     #'some.user@example.com': 'someuser',
105 }
106
107 # These emails are removed from CC list
108 IGNORE_CC = [
109     #'loser@example.com',
110 ]
111
112 # The 'component' field in Trac can come either from the Product or
113 # or from the Component field of Bugzilla. COMPONENTS_FROM_PRODUCTS
114 # switches the behavior.
115 # If COMPONENTS_FROM_PRODUCTS is True:
116 # - Bugzilla Product -> Trac Component
117 # - Bugzilla Component -> Trac Keyword
118 # IF COMPONENTS_FROM_PRODUCTS is False:
119 # - Bugzilla Product -> Trac Keyword
120 # - Bugzilla Component -> Trac Component
121 COMPONENTS_FROM_PRODUCTS = False
122
123 # If COMPONENTS_FROM_PRODUCTS is True, the default owner for each
124 # Trac component is inferred from a default Bugzilla component.
125 DEFAULT_COMPONENTS = ["default", "misc", "main"]
126
127 # This mapping can assign keywords in the ticket entry to represent
128 # products or components (depending on COMPONENTS_FROM_PRODUCTS).
129 # The keyword will be ignored if empty.
130 KEYWORDS_MAPPING = {
131     #'Bugzilla_product_or_component': 'Keyword',
132     "default": "",
133     "misc": "",
134     }
135
136 # If this is True, products or components are all set as keywords
137 # even if not mentionned in KEYWORDS_MAPPING.
138 MAP_ALL_KEYWORDS = True
139
140
141 # Bug comments that should not be imported.  Each entry in list should
142 # be a regular expression.
143 IGNORE_COMMENTS = [
144    "^Created an attachment \(id="
145 ]
146
147 ###########################################################################
148 ### You probably don't need to change any configuration past this line. ###
149 ###########################################################################
150
151 # Bugzilla status to Trac status translation map.
152 #
153 # NOTE: bug activity is translated as well, which may cause bug
154 # activity to be deleted (e.g. resolved -> closed in Bugzilla
155 # would translate into closed -> closed in Trac, so we just ignore the
156 # change).
157 #
158 # There is some special magic for open in the code:  if there is no
159 # Bugzilla owner, open is mapped to 'new' instead.
160 STATUS_TRANSLATE = {
161   "unconfirmed": "new",
162   "open":        "assigned",
163   "resolved":    "closed",
164   "verified":    "closed",
165   "released":    "closed"
166 }
167
168 # Translate Bugzilla statuses into Trac keywords.  This provides a way
169 # to retain the Bugzilla statuses in Trac.  e.g. when a bug is marked
170 # 'verified' in Bugzilla it will be assigned a VERIFIED keyword.
171 STATUS_KEYWORDS = {
172   "verified": "VERIFIED",
173   "released": "RELEASED"
174 }
175
176 # Some fields in Bugzilla do not have equivalents in Trac.  Changes in
177 # fields listed here will not be imported into the ticket change history,
178 # otherwise you'd see changes for fields that don't exist in Trac.
179 IGNORED_ACTIVITY_FIELDS = ["everconfirmed"]
180
181 # Regular expression and its replacement
182 BUG_NO_RE = re.compile(r"\b(bug #?)([0-9])")
183 BUG_NO_REPL = r"#\2"
184
185 ###
186 ### Script begins here
187 ###
188
189 import os
190 import sys
191 import string
192 import StringIO
193
194 import MySQLdb
195 import MySQLdb.cursors
196 try:
197     from trac.env import Environment
198 except:
199     from trac.Environment import Environment
200 from trac.attachment import Attachment
201
202 if not hasattr(sys, 'setdefaultencoding'):
203     reload(sys)
204
205 sys.setdefaultencoding('latin1')
206
207 # simulated Attachment class for trac.add
208 #class Attachment:
209 #    def __init__(self, name, data):
210 #        self.filename = name
211 #        self.file = StringIO.StringIO(data.tostring())
212
213 # simple field translation mapping.  if string not in
214 # mapping, just return string, otherwise return value
215 class FieldTranslator(dict):
216     def __getitem__(self, item):
217         if not dict.has_key(self, item):
218             return item
219
220         return dict.__getitem__(self, item)
221
222 statusXlator = FieldTranslator(STATUS_TRANSLATE)
223
224 class TracDatabase(object):
225     def __init__(self, path):
226         self.env = Environment(path)
227         self._db = self.env.get_db_cnx()
228         self._db.autocommit = False
229         self.loginNameCache = {}
230         self.fieldNameCache = {}
231
232     def db(self):
233         return self._db
234
235     def hasTickets(self):
236         c = self.db().cursor()
237         c.execute("SELECT count(*) FROM Ticket")
238         return int(c.fetchall()[0][0]) > 0
239
240     def assertNoTickets(self):
241         if self.hasTickets():
242             raise Exception("Will not modify database with existing tickets!")
243
244     def setSeverityList(self, s):
245         """Remove all severities, set them to `s`"""
246         self.assertNoTickets()
247
248         c = self.db().cursor()
249         c.execute("DELETE FROM enum WHERE type='severity'")
250         for value, i in s:
251             print "  inserting severity '%s' - '%s'" % (value, i)
252             c.execute("""INSERT INTO enum (type, name, value)
253                                    VALUES (%s, %s, %s)""",
254                       ("severity", value.encode('utf-8'), i))
255         self.db().commit()
256
257     def setPriorityList(self, s):
258         """Remove all priorities, set them to `s`"""
259         self.assertNoTickets()
260
261         c = self.db().cursor()
262         c.execute("DELETE FROM enum WHERE type='priority'")
263         for value, i in s:
264             print "  inserting priority '%s' - '%s'" % (value, i)
265             c.execute("""INSERT INTO enum (type, name, value)
266                                    VALUES (%s, %s, %s)""",
267                       ("priority", value.encode('utf-8'), i))
268         self.db().commit()
269
270
271     def setComponentList(self, l, key):
272         """Remove all components, set them to `l`"""
273         self.assertNoTickets()
274
275         c = self.db().cursor()
276         c.execute("DELETE FROM component")
277         for comp in l:
278             print "  inserting component '%s', owner '%s'" % \
279                             (comp[key], comp['owner'])
280             c.execute("INSERT INTO component (name, owner) VALUES (%s, %s)",
281                       (comp[key].encode('utf-8'),
282                        comp['owner'].encode('utf-8')))
283         self.db().commit()
284
285     def setVersionList(self, v, key):
286         """Remove all versions, set them to `v`"""
287         self.assertNoTickets()
288
289         c = self.db().cursor()
290         c.execute("DELETE FROM version")
291         for vers in v:
292             print "  inserting version '%s'" % (vers[key])
293             c.execute("INSERT INTO version (name) VALUES (%s)",
294                       (vers[key].encode('utf-8'),))
295         self.db().commit()
296
297     def setMilestoneList(self, m, key):
298         """Remove all milestones, set them to `m`"""
299         self.assertNoTickets()
300
301         c = self.db().cursor()
302         c.execute("DELETE FROM milestone")
303         for ms in m:
304             milestone = ms[key]
305             print "  inserting milestone '%s'" % (milestone)
306             c.execute("INSERT INTO milestone (name) VALUES (%s)",
307                       (milestone.encode('utf-8'),))
308         self.db().commit()
309
310     def addTicket(self, id, time, changetime, component, severity, priority,
311                   owner, reporter, cc, version, milestone, status, resolution,
312                   summary, description, keywords):
313         c = self.db().cursor()
314
315         desc = description.encode('utf-8')
316         type = "defect"
317
318         if severity.lower() == "enhancement":
319                 severity = "minor"
320                 type = "enhancement"
321
322         if PREFORMAT_COMMENTS:
323           desc = '{{{\n%s\n}}}' % desc
324
325         if REPLACE_BUG_NO:
326             if BUG_NO_RE.search(desc):
327                 desc = re.sub(BUG_NO_RE, BUG_NO_REPL, desc)
328
329         if PRIORITIES_MAP.has_key(priority):
330             priority = PRIORITIES_MAP[priority]
331
332         print "  inserting ticket %s -- %s" % (id, summary)
333
334         c.execute("""INSERT INTO ticket (id, type, time, changetime, component,
335                                          severity, priority, owner, reporter,
336                                          cc, version, milestone, status,
337                                          resolution, summary, description,
338                                          keywords)
339                                  VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s,
340                                          %s, %s, %s, %s, %s, %s, %s, %s)""",
341                   (id, type.encode('utf-8'), datetime2epoch(time),
342                    datetime2epoch(changetime), component.encode('utf-8'),
343                    severity.encode('utf-8'), priority.encode('utf-8'), owner,
344                    reporter, cc, version, milestone.encode('utf-8'),
345                    status.lower(), resolution, summary.encode('utf-8'), desc,
346                    keywords))
347
348         self.db().commit()
349         return self.db().get_last_id(c, 'ticket')
350
351     def addTicketComment(self, ticket, time, author, value):
352         comment = value.encode('utf-8')
353
354         if PREFORMAT_COMMENTS:
355           comment = '{{{\n%s\n}}}' % comment
356
357         if REPLACE_BUG_NO:
358             if BUG_NO_RE.search(comment):
359                 comment = re.sub(BUG_NO_RE, BUG_NO_REPL, comment)
360
361         c = self.db().cursor()
362         c.execute("""INSERT INTO ticket_change (ticket, time, author, field,
363                                                 oldvalue, newvalue)
364                                         VALUES (%s, %s, %s, %s, %s, %s)""",
365                   (ticket, datetime2epoch(time), author, 'comment', '', comment))
366         self.db().commit()
367
368     def addTicketChange(self, ticket, time, author, field, oldvalue, newvalue):
369         c = self.db().cursor()
370
371         if field == "owner":
372             if LOGIN_MAP.has_key(oldvalue):
373                 oldvalue = LOGIN_MAP[oldvalue]
374             if LOGIN_MAP.has_key(newvalue):
375                 newvalue = LOGIN_MAP[newvalue]
376
377         if field == "priority":
378             if PRIORITIES_MAP.has_key(oldvalue.lower()):
379                 oldvalue = PRIORITIES_MAP[oldvalue.lower()]
380             if PRIORITIES_MAP.has_key(newvalue.lower()):
381                 newvalue = PRIORITIES_MAP[newvalue.lower()]
382
383         # Doesn't make sense if we go from highest -> highest, for example.
384         if oldvalue == newvalue:
385             return
386
387         c.execute("""INSERT INTO ticket_change (ticket, time, author, field,
388                                                 oldvalue, newvalue)
389                                         VALUES (%s, %s, %s, %s, %s, %s)""",
390                   (ticket, datetime2epoch(time), author, field,
391                    oldvalue.encode('utf-8'), newvalue.encode('utf-8')))
392         self.db().commit()
393
394     def addAttachment(self, author, a):
395         description = a['description'].encode('utf-8')
396         id = a['bug_id']
397         filename = a['filename'].encode('utf-8')
398         filedata = StringIO.StringIO(a['thedata'])
399         filesize = len(filedata.getvalue())
400         time = a['creation_ts']
401         print "    ->inserting attachment '%s' for ticket %s -- %s" % \
402                 (filename, id, description)
403
404         attachment = Attachment(self.env, 'ticket', id)
405         attachment.author = author
406         attachment.description = description
407         attachment.insert(filename, filedata, filesize, datetime2epoch(time))
408         del attachment
409
410     def getLoginName(self, cursor, userid):
411         if userid not in self.loginNameCache:
412             cursor.execute("SELECT * FROM profiles WHERE userid = %s", (userid))
413             loginName = cursor.fetchall()
414
415             if loginName:
416                 loginName = loginName[0]['login_name']
417             else:
418                 print """WARNING: unknown bugzilla userid %d, recording as
419                          anonymous""" % (userid)
420                 loginName = "anonymous"
421
422             loginName = LOGIN_MAP.get(loginName, loginName)
423
424             self.loginNameCache[userid] = loginName
425
426         return self.loginNameCache[userid]
427
428     def getFieldName(self, cursor, fieldid):
429         if fieldid not in self.fieldNameCache:
430             # fielddefs.fieldid got changed to fielddefs.id in Bugzilla
431             # 2.23.3.
432             if BZ_VERSION >= 2233:
433                 cursor.execute("SELECT * FROM fielddefs WHERE id = %s",
434                                (fieldid))
435             else:
436                 cursor.execute("SELECT * FROM fielddefs WHERE fieldid = %s",
437                                (fieldid))
438             fieldName = cursor.fetchall()
439
440             if fieldName:
441                 fieldName = fieldName[0]['name'].lower()
442             else:
443                 print "WARNING: unknown bugzilla fieldid %d, \
444                                 recording as unknown" % (userid)
445                 fieldName = "unknown"
446
447             self.fieldNameCache[fieldid] = fieldName
448
449         return self.fieldNameCache[fieldid]
450
451 def makeWhereClause(fieldName, values, negative=False):
452     if not values:
453         return ''
454     if negative:
455         connector, op = ' AND ', '!='
456     else:
457         connector, op = ' OR ', '='
458     clause = connector.join(["%s %s '%s'" % (fieldName, op, value)
459                              for value in values])
460     return ' ' + clause
461
462 def convert(_db, _host, _user, _password, _env, _force):
463     activityFields = FieldTranslator()
464
465     # account for older versions of bugzilla
466     print "Using Bugzilla v%s schema." % BZ_VERSION
467     if BZ_VERSION == 2110:
468         activityFields['removed'] = "oldvalue"
469         activityFields['added'] = "newvalue"
470
471     # init Bugzilla environment
472     print "Bugzilla MySQL('%s':'%s':'%s':'%s'): connecting..." % \
473             (_db, _host, _user, ("*" * len(_password)))
474     mysql_con = MySQLdb.connect(host=_host,
475                 user=_user, passwd=_password, db=_db, compress=1,
476                 cursorclass=MySQLdb.cursors.DictCursor)
477     mysql_cur = mysql_con.cursor()
478
479     # init Trac environment
480     print "Trac SQLite('%s'): connecting..." % (_env)
481     trac = TracDatabase(_env)
482
483     # force mode...
484     if _force == 1:
485         print "\nCleaning all tickets..."
486         c = trac.db().cursor()
487         c.execute("DELETE FROM ticket_change")
488         trac.db().commit()
489
490         c.execute("DELETE FROM ticket")
491         trac.db().commit()
492
493         c.execute("DELETE FROM attachment")
494         attachments_dir = os.path.join(os.path.normpath(trac.env.path),
495                                 "attachments")
496         # Straight from the Python documentation.
497         for root, dirs, files in os.walk(attachments_dir, topdown=False):
498             for name in files:
499                 os.remove(os.path.join(root, name))
500             for name in dirs:
501                 os.rmdir(os.path.join(root, name))
502         if not os.stat(attachments_dir):
503             os.mkdir(attachments_dir)
504         trac.db().commit()
505         print "All tickets cleaned..."
506
507
508     print "\n0. Filtering products..."
509     if BZ_VERSION >= 2180:
510         mysql_cur.execute("SELECT name FROM products")
511     else:
512         mysql_cur.execute("SELECT product AS name FROM products")
513     products = []
514     for line in mysql_cur.fetchall():
515         product = line['name']
516         if PRODUCTS and product not in PRODUCTS:
517             continue
518         if product in IGNORE_PRODUCTS:
519             continue
520         products.append(product)
521     PRODUCTS[:] = products
522     print "  Using products", " ".join(PRODUCTS)
523
524     print "\n1. Import severities..."
525     trac.setSeverityList(SEVERITIES)
526
527     print "\n2. Import components..."
528     if not COMPONENTS_FROM_PRODUCTS:
529         if BZ_VERSION >= 2180:
530             sql = """SELECT DISTINCT c.name AS name, c.initialowner AS owner
531                                FROM components AS c, products AS p
532                                WHERE c.product_id = p.id AND"""
533             sql += makeWhereClause('p.name', PRODUCTS)
534         else:
535             sql = "SELECT value AS name, initialowner AS owner FROM components"
536             sql += " WHERE" + makeWhereClause('program', PRODUCTS)
537         mysql_cur.execute(sql)
538         components = mysql_cur.fetchall()
539         for component in components:
540             component['owner'] = trac.getLoginName(mysql_cur,
541                                                    component['owner'])
542         trac.setComponentList(components, 'name')
543     else:
544         if BZ_VERSION >= 2180:
545             sql = ("SELECT p.name AS product, c.name AS comp, "
546                    " c.initialowner AS owner "
547                    "FROM components c, products p "
548                    "WHERE c.product_id = p.id and " +
549                    makeWhereClause('p.name', PRODUCTS))
550         else:
551             sql = ("SELECT program AS product, value AS comp, "
552                    " initialowner AS owner "
553                    "FROM components WHERE" +
554                    makeWhereClause('program', PRODUCTS))
555         mysql_cur.execute(sql)
556         lines = mysql_cur.fetchall()
557         all_components = {} # product -> components
558         all_owners = {} # product, component -> owner
559         for line in lines:
560             product = line['product']
561             comp = line['comp']
562             owner = line['owner']
563             all_components.setdefault(product, []).append(comp)
564             all_owners[(product, comp)] = owner
565         component_list = []
566         for product, components in all_components.items():
567             # find best default owner
568             default = None
569             for comp in DEFAULT_COMPONENTS:
570                 if comp in components:
571                     default = comp
572                     break
573             if default is None:
574                 default = components[0]
575             owner = all_owners[(product, default)]
576             owner_name = trac.getLoginName(mysql_cur, owner)
577             component_list.append({'product': product, 'owner': owner_name})
578         trac.setComponentList(component_list, 'product')
579
580     print "\n3. Import priorities..."
581     trac.setPriorityList(PRIORITIES)
582
583     print "\n4. Import versions..."
584     if BZ_VERSION >= 2180:
585         sql = """SELECT DISTINCTROW versions.value AS value
586                                FROM products, versions"""
587         sql += " WHERE" + makeWhereClause('products.name', PRODUCTS)
588     else:
589         sql = "SELECT DISTINCTROW value FROM versions"
590         sql += " WHERE" + makeWhereClause('program', PRODUCTS)
591     mysql_cur.execute(sql)
592     versions = mysql_cur.fetchall()
593     trac.setVersionList(versions, 'value')
594
595     print "\n5. Import milestones..."
596     sql = "SELECT DISTINCT value FROM milestones"
597     sql += " WHERE" + makeWhereClause('value', IGNORE_MILESTONES, negative=True)
598     mysql_cur.execute(sql)
599     milestones = mysql_cur.fetchall()
600     trac.setMilestoneList(milestones, 'value')
601
602     print "\n6. Retrieving bugs..."
603     sql = """SELECT DISTINCT b.*, c.name AS component, p.name AS product
604                         FROM bugs AS b, components AS c, products AS p """
605     sql += " WHERE (" + makeWhereClause('p.name', PRODUCTS)
606     sql += ") AND b.product_id = p.id"
607     sql += " AND b.component_id = c.id"
608     sql += " ORDER BY b.bug_id"
609     mysql_cur.execute(sql)
610     bugs = mysql_cur.fetchall()
611
612
613     print "\n7. Import bugs and bug activity..."
614     for bug in bugs:
615         bugid = bug['bug_id']
616
617         ticket = {}
618         keywords = []
619         ticket['id'] = bugid
620         ticket['time'] = bug['creation_ts']
621         ticket['changetime'] = bug['delta_ts']
622         if COMPONENTS_FROM_PRODUCTS:
623             ticket['component'] = bug['product']
624         else:
625             ticket['component'] = bug['component']
626         ticket['severity'] = bug['bug_severity']
627         ticket['priority'] = bug['priority'].lower()
628
629         ticket['owner'] = trac.getLoginName(mysql_cur, bug['assigned_to'])
630         ticket['reporter'] = trac.getLoginName(mysql_cur, bug['reporter'])
631
632         mysql_cur.execute("SELECT * FROM cc WHERE bug_id = %s", bugid)
633         cc_records = mysql_cur.fetchall()
634         cc_list = []
635         for cc in cc_records:
636             cc_list.append(trac.getLoginName(mysql_cur, cc['who']))
637         cc_list = [cc for cc in cc_list if '@' in cc and cc not in IGNORE_CC]
638         ticket['cc'] = string.join(cc_list, ', ')
639
640         ticket['version'] = bug['version']
641
642         target_milestone = bug['target_milestone']
643         if target_milestone in IGNORE_MILESTONES:
644             target_milestone = ''
645         ticket['milestone'] = target_milestone
646
647         bug_status = bug['bug_status'].lower()
648         ticket['status'] = statusXlator[bug_status]
649         ticket['resolution'] = bug['resolution'].lower()
650
651         # a bit of extra work to do open tickets
652         if bug_status == 'open':
653             if owner != '':
654                 ticket['status'] = 'assigned'
655             else:
656                 ticket['status'] = 'new'
657
658         ticket['summary'] = bug['short_desc']
659
660         mysql_cur.execute("SELECT * FROM longdescs WHERE bug_id = %s" % bugid)
661         longdescs = list(mysql_cur.fetchall())
662
663         # check for empty 'longdescs[0]' field...
664         if len(longdescs) == 0:
665             ticket['description'] = ''
666         else:
667             ticket['description'] = longdescs[0]['thetext']
668             del longdescs[0]
669
670         for desc in longdescs:
671             ignore = False
672             for comment in IGNORE_COMMENTS:
673                 if re.match(comment, desc['thetext']):
674                     ignore = True
675
676             if ignore:
677                     continue
678
679             trac.addTicketComment(ticket=bugid,
680                 time = desc['bug_when'],
681                 author=trac.getLoginName(mysql_cur, desc['who']),
682                 value = desc['thetext'])
683
684         mysql_cur.execute("""SELECT * FROM bugs_activity WHERE bug_id = %s
685                            ORDER BY bug_when""" % bugid)
686         bugs_activity = mysql_cur.fetchall()
687         resolution = ''
688         ticketChanges = []
689         keywords = []
690         for activity in bugs_activity:
691             field_name = trac.getFieldName(mysql_cur, activity['fieldid']).lower()
692
693             removed = activity[activityFields['removed']]
694             added = activity[activityFields['added']]
695
696             # statuses and resolutions are in lowercase in trac
697             if field_name == "resolution" or field_name == "bug_status":
698                 removed = removed.lower()
699                 added = added.lower()
700
701             # remember most recent resolution, we need this later
702             if field_name == "resolution":
703                 resolution = added.lower()
704
705             add_keywords = []
706             remove_keywords = []
707
708             # convert bugzilla field names...
709             if field_name == "bug_severity":
710                 field_name = "severity"
711             elif field_name == "assigned_to":
712                 field_name = "owner"
713             elif field_name == "bug_status":
714                 field_name = "status"
715                 if removed in STATUS_KEYWORDS:
716                     remove_keywords.append(STATUS_KEYWORDS[removed])
717                 if added in STATUS_KEYWORDS:
718                     add_keywords.append(STATUS_KEYWORDS[added])
719                 added = statusXlator[added]
720                 removed = statusXlator[removed]
721             elif field_name == "short_desc":
722                 field_name = "summary"
723             elif field_name == "product" and COMPONENTS_FROM_PRODUCTS:
724                 field_name = "component"
725             elif ((field_name == "product" and not COMPONENTS_FROM_PRODUCTS) or
726                   (field_name == "component" and COMPONENTS_FROM_PRODUCTS)):
727                 if MAP_ALL_KEYWORDS or removed in KEYWORDS_MAPPING:
728                     kw = KEYWORDS_MAPPING.get(removed, removed)
729                     if kw:
730                         remove_keywords.append(kw)
731                 if MAP_ALL_KEYWORDS or added in KEYWORDS_MAPPING:
732                     kw = KEYWORDS_MAPPING.get(added, added)
733                     if kw:
734                         add_keywords.append(kw)
735                 if field_name == "component":
736                     # just keep the keyword change
737                     added = removed = ""
738             elif field_name == "target_milestone":
739                 field_name = "milestone"
740                 if added in IGNORE_MILESTONES:
741                     added = ""
742                 if removed in IGNORE_MILESTONES:
743                     removed = ""
744
745             ticketChange = {}
746             ticketChange['ticket'] = bugid
747             ticketChange['time'] = activity['bug_when']
748             ticketChange['author'] = trac.getLoginName(mysql_cur,
749                                                        activity['who'])
750             ticketChange['field'] = field_name
751             ticketChange['oldvalue'] = removed
752             ticketChange['newvalue'] = added
753
754             if add_keywords or remove_keywords:
755                 # ensure removed ones are in old
756                 old_keywords = keywords + [kw for kw in remove_keywords if kw
757                                            not in keywords]
758                 # remove from new
759                 keywords = [kw for kw in keywords if kw not in remove_keywords]
760                 # add to new
761                 keywords += [kw for kw in add_keywords if kw not in keywords]
762                 if old_keywords != keywords:
763                     ticketChangeKw = ticketChange.copy()
764                     ticketChangeKw['field'] = "keywords"
765                     ticketChangeKw['oldvalue'] = ' '.join(old_keywords)
766                     ticketChangeKw['newvalue'] = ' '.join(keywords)
767                     ticketChanges.append(ticketChangeKw)
768
769             if field_name in IGNORED_ACTIVITY_FIELDS:
770                 continue
771
772             # Skip changes that have no effect (think translation!).
773             if added == removed:
774                 continue
775
776             # Bugzilla splits large summary changes into two records.
777             for oldChange in ticketChanges:
778               if (field_name == "summary"
779                   and oldChange['field'] == ticketChange['field']
780                   and oldChange['time'] == ticketChange['time']
781                   and oldChange['author'] == ticketChange['author']):
782                   oldChange['oldvalue'] += " " + ticketChange['oldvalue']
783                   oldChange['newvalue'] += " " + ticketChange['newvalue']
784                   break
785               # cc sometime appear in different activities with same time
786               if (field_name == "cc" \
787                   and oldChange['time'] == ticketChange['time']):
788                   oldChange['newvalue'] += ", " + ticketChange['newvalue']
789                   break
790             else:
791                 ticketChanges.append (ticketChange)
792
793         for ticketChange in ticketChanges:
794             trac.addTicketChange (**ticketChange)
795
796         # For some reason, bugzilla v2.11 seems to clear the resolution
797         # when you mark a bug as closed.  Let's remember it and restore
798         # it if the ticket is closed but there's no resolution.
799         if not ticket['resolution'] and ticket['status'] == "closed":
800             ticket['resolution'] = resolution
801
802         bug_status = bug['bug_status']
803         if bug_status in STATUS_KEYWORDS:
804             kw = STATUS_KEYWORDS[bug_status]
805             if kw not in keywords:
806                 keywords.append(kw)
807
808         product = bug['product']
809         if product in KEYWORDS_MAPPING and not COMPONENTS_FROM_PRODUCTS:
810             kw = KEYWORDS_MAPPING.get(product, product)
811             if kw and kw not in keywords:
812                 keywords.append(kw)
813
814         component = bug['component']
815         if (COMPONENTS_FROM_PRODUCTS and \
816             (MAP_ALL_KEYWORDS or component in KEYWORDS_MAPPING)):
817             kw = KEYWORDS_MAPPING.get(component, component)
818             if kw and kw not in keywords:
819                 keywords.append(kw)
820
821         ticket['keywords'] = string.join(keywords)
822         ticketid = trac.addTicket(**ticket)
823
824         if BZ_VERSION >= 2180:
825             mysql_cur.execute("SELECT attachments.*, attach_data.thedata "
826                               "FROM attachments, attach_data "
827                               "WHERE attachments.bug_id = %s AND "
828                               "attachments.attach_id = attach_data.id" % bugid)
829         else:
830             mysql_cur.execute("SELECT * FROM attachments WHERE bug_id = %s" %
831                               bugid)
832         attachments = mysql_cur.fetchall()
833         for a in attachments:
834             author = trac.getLoginName(mysql_cur, a['submitter_id'])
835             trac.addAttachment(author, a)
836
837     print "\n8. Importing users and passwords..."
838     if BZ_VERSION >= 2180:
839         mysql_cur.execute("SELECT login_name, cryptpassword FROM profiles")
840         users = mysql_cur.fetchall()
841     htpasswd = file("htpasswd", 'w')
842     for user in users:
843         if LOGIN_MAP.has_key(user['login_name']):
844             login = LOGIN_MAP[user['login_name']]
845         else:
846             login = user['login_name']
847         htpasswd.write(login + ":" + user['cryptpassword'] + "\n")
848
849     htpasswd.close()
850     print "  Bugzilla users converted to htpasswd format, see 'htpasswd'."
851
852     print "\nAll tickets converted."
853
854 def log(msg):
855     print "DEBUG: %s" % (msg)
856
857 def datetime2epoch(dt) :
858     import time
859     return time.mktime(dt.timetuple())
860
861 def usage():
862     print """bugzilla2trac - Imports a bug database from Bugzilla into Trac.
863
864 Usage: bugzilla2trac.py [options]
865
866 Available Options:
867   --db <MySQL dbname>              - Bugzilla's database name
868   --tracenv /path/to/trac/env      - Full path to Trac db environment
869   -h | --host <MySQL hostname>     - Bugzilla's DNS host name
870   -u | --user <MySQL username>     - Effective Bugzilla's database user
871   -p | --passwd <MySQL password>   - Bugzilla's user password
872   -c | --clean                     - Remove current Trac tickets before
873                                      importing
874   --help | help                    - This help info
875
876 Additional configuration options can be defined directly in the script.
877 """
878     sys.exit(0)
879
880 def main():
881     global BZ_DB, BZ_HOST, BZ_USER, BZ_PASSWORD, TRAC_ENV, TRAC_CLEAN
882     if len (sys.argv) > 1:
883         if sys.argv[1] in ['--help','help'] or len(sys.argv) < 4:
884             usage()
885         iter = 1
886         while iter < len(sys.argv):
887             if sys.argv[iter] in ['--db'] and iter+1 < len(sys.argv):
888                 BZ_DB = sys.argv[iter+1]
889                 iter = iter + 1
890             elif sys.argv[iter] in ['-h', '--host'] and iter+1 < len(sys.argv):
891                 BZ_HOST = sys.argv[iter+1]
892                 iter = iter + 1
893             elif sys.argv[iter] in ['-u', '--user'] and iter+1 < len(sys.argv):
894                 BZ_USER = sys.argv[iter+1]
895                 iter = iter + 1
896             elif sys.argv[iter] in ['-p', '--passwd'] and iter+1 < len(sys.argv):
897                 BZ_PASSWORD = sys.argv[iter+1]
898                 iter = iter + 1
899             elif sys.argv[iter] in ['--tracenv'] and iter+1 < len(sys.argv):
900                 TRAC_ENV = sys.argv[iter+1]
901                 iter = iter + 1
902             elif sys.argv[iter] in ['-c', '--clean']:
903                 TRAC_CLEAN = 1
904             else:
905                 print "Error: unknown parameter: " + sys.argv[iter]
906                 sys.exit(0)
907             iter = iter + 1
908
909     convert(BZ_DB, BZ_HOST, BZ_USER, BZ_PASSWORD, TRAC_ENV, TRAC_CLEAN)
910
911 if __name__ == '__main__':
912     main()
Note: See TracBrowser for help on using the browser.

SFLC Main Page

[frdm] Support SFLC