Edgewall Software

source: trunk/contrib/bugzilla2trac.py

Last change on this file was 13167, checked in by rjollos, 9 months ago

1.1.3dev: Merged [13166] from 1.0-stable. Refs #11787.

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