Edgewall Software

root/trunk/contrib/bugzilla2trac.py

Revision 8272, 35.5 KB (checked in by cboos, 2 weeks ago)

0.12dev: merged from stable [8229,8231,8233-8236,8264-8265,8269/branches/0.11-stable].

What is interesting here is that the merge was done using plain svn merge ../0.11-stable (svn 1.6.2) and the result seems quite good. Finally time to ditch svnmerge.py?

I'll fix the svnmerge-integrated property on the next commit.

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