Edgewall Software

Ticket #1462: bugzilla2trac.py.diff

File bugzilla2trac.py.diff, 19.2 KB (added by Florent Guillaume <fg@…>, 4 years ago)
  • bugzilla2trac.py

    old new  
    1313Copyright 2004, Dmitry Yusupov <dmitry_yus@yahoo.com> 
    1414 
    1515Many enhancements, Bill Soudan <bill@soudan.net> 
     16Other enhancements, Florent Guillaume <fg@nuxeo.com> 
    1617""" 
    1718 
     19import re 
     20 
    1821### 
    1922### Conversion Settings -- edit these before running if desired 
    2023### 
     
    2225# Bugzilla version.  You can find this in Bugzilla's globals.pl file. 
    2326# 
    2427# Currently, the following bugzilla versions are known to work: 
    25 #   2.11 
     28#   2.11, 2.16.5 
    2629# 
    2730# If you run this script on a version not listed here and it is successful, 
    2831# please report it to the Trac mailing list so we can update the list. 
     
    3033 
    3134# MySQL connection parameters for the Bugzilla database.  These can also  
    3235# be specified on the command line. 
    33 BZ_DB = '' 
     36BZ_DB = 'bugs' 
    3437BZ_HOST = 'localhost' 
    3538BZ_USER = '' 
    3639BZ_PASSWORD = '' 
     
    4649# preformat block?  This formats the text in a fixed-point font. 
    4750PREFORMAT_COMMENTS = False 
    4851 
     52# Replace bug numbers in comments with #xyz 
     53REPLACE_BUG_NO = False 
     54 
     55# Severities 
     56SEVERITIES = [ 
     57    ('blocker',     '1'), 
     58    ('critical',    '2'), 
     59    ('major',       '3'), 
     60    ('normal',      '4'), 
     61    ('minor',       '5'), 
     62    ('trivial',     '6'), 
     63    ('enhancement', '7'), 
     64    ] 
     65 
     66# Priorities 
     67PRIORITIES = [ 
     68    ('P1', '1'), 
     69    ('P2', '2'), 
     70    ('P3', '3'), 
     71    ('P4', '4'), 
     72    ('P5', '5'), 
     73    ] 
     74 
    4975# By default, all bugs are imported from Bugzilla.  If you add a list 
    5076# of products here, only bugs from those products will be imported. 
    5177PRODUCTS = [] 
     78# These Bugzilla products will be ignored during import. 
     79IGNORE_PRODUCTS = [] 
     80 
     81# These milestones are ignored 
     82IGNORE_MILESTONES = ['---'] 
     83 
     84# These logins are converted to these user ids 
     85LOGIN_MAP = { 
     86    #'some.user@example.com': 'someuser', 
     87   } 
     88 
     89# These emails are removed from CC list 
     90IGNORE_CC = [ 
     91    #'loser@example.com', 
     92    ] 
     93 
     94# The 'component' field in Trac can come either from the Product or 
     95# or from the Component field of Bugzilla. COMPONENTS_FROM_PRODUCTS 
     96# switches the behavior. 
     97# If COMPONENTS_FROM_PRODUCTS is True: 
     98# - Bugzilla Product -> Trac Component 
     99# - Bugzilla Component -> Trac Keyword 
     100# IF COMPONENTS_FROM_PRODUCTS is False: 
     101# - Bugzilla Product -> Trac Keyword 
     102# - Bugzilla Component -> Trac Component 
     103COMPONENTS_FROM_PRODUCTS = False 
     104 
     105# If COMPONENTS_FROM_PRODUCTS is True, the default owner for each 
     106# Trac component is inferred from a default Bugzilla component. 
     107DEFAULT_COMPONENTS = ['default', 'misc', 'main'] 
     108 
     109# This mapping can assign keywords in the ticket entry to represent 
     110# products or components (depending on COMPONENTS_FROM_PRODUCTS). 
     111# The keyword will be ignored if empty. 
     112KEYWORDS_MAPPING = { 
     113    #'Bugzilla_product_or_component': 'Keyword', 
     114    'default': '', 
     115    'misc': '', 
     116    } 
     117 
     118# If this is True, products or components are all set as keywords 
     119# even if not mentionned in KEYWORDS_MAPPING. 
     120MAP_ALL_KEYWORDS = True 
    52121 
    53 # Trac doesn't have the concept of a product.  Instead, this script can 
    54 # assign keywords in the ticket entry to represent products. 
    55 # 
    56 # ex. PRODUCT_KEYWORDS = { 'product1' : 'PRODUCT1_KEYWORD' } 
    57 PRODUCT_KEYWORDS = {} 
    58122 
    59123# Bug comments that should not be imported.  Each entry in list should 
    60124# be a regular expression. 
     
    96160# otherwise you'd see changes for fields that don't exist in Trac. 
    97161IGNORED_ACTIVITY_FIELDS = ['everconfirmed'] 
    98162 
     163# Regular expression and its replacement 
     164BUG_NO_RE = re.compile(r'\b(bug #?)([0-9])') 
     165BUG_NO_REPL = r'#\2' 
     166 
    99167### 
    100168### Script begins here 
    101169### 
    102170 
    103171import os 
    104 import re 
    105172import sys 
    106173import string 
    107174import StringIO 
    108175 
    109176import MySQLdb 
    110177import MySQLdb.cursors 
    111 import trac.env 
     178try: 
     179    from trac.env import Environment 
     180except: 
     181    from trac.Environment import Environment 
    112182 
    113183if not hasattr(sys, 'setdefaultencoding'): 
    114184    reload(sys) 
     
    134204 
    135205class TracDatabase(object): 
    136206    def __init__(self, path): 
    137         self.env = trac.env.Environment(path) 
     207        self.env = Environment(path) 
    138208        self._db = self.env.get_db_cnx() 
    139209        self._db.autocommit = False 
    140210        self.loginNameCache = {} 
     
    210280        c = self.db().cursor() 
    211281        c.execute("""DELETE FROM milestone""") 
    212282        for ms in m: 
    213             print "inserting milestone ", ms[key] 
     283            milestone = ms[key] 
     284            print "inserting milestone ", milestone 
    214285            c.execute("""INSERT INTO milestone (name) VALUES (%s)""", 
    215                       ms[key].encode('utf-8')) 
     286                      milestone.encode('utf-8')) 
    216287        self.db().commit() 
    217288     
    218289    def addTicket(self, id, time, changetime, component, 
     
    226297        if PREFORMAT_COMMENTS: 
    227298          desc = '{{{\n%s\n}}}' % desc 
    228299 
     300        if REPLACE_BUG_NO: 
     301            if BUG_NO_RE.search(desc): 
     302                desc = re.sub(BUG_NO_RE, BUG_NO_REPL, desc) 
     303 
    229304        print "inserting ticket %s -- %s" % (id, summary) 
    230305        c.execute("""INSERT INTO ticket (id, time, changetime, component, 
    231306                                         severity, priority, owner, reporter, cc, 
     
    249324        if PREFORMAT_COMMENTS: 
    250325          comment = '{{{\n%s\n}}}' % comment 
    251326 
     327        if REPLACE_BUG_NO: 
     328            if BUG_NO_RE.search(comment): 
     329                comment = re.sub(BUG_NO_RE, BUG_NO_REPL, comment) 
     330 
    252331        c = self.db().cursor() 
    253332        c.execute("""INSERT INTO ticket_change (ticket, time, author, field, oldvalue, newvalue) 
    254333                                 VALUES        (%s, %s, %s, %s, %s, %s)""", 
     
    279358                print 'warning: unknown bugzilla userid %d, recording as anonymous' % userid 
    280359                loginName = 'anonymous' 
    281360 
     361            loginName = LOGIN_MAP.get(loginName, loginName) 
     362 
    282363            self.loginNameCache[userid] = loginName 
    283364 
    284365        return self.loginNameCache[userid] 
     
    298379 
    299380        return self.fieldNameCache[fieldid] 
    300381 
    301 def productFilter(fieldName, products): 
    302     first = True 
    303     result = '' 
    304     for product in products: 
    305         if not first:  
    306             result += " or " 
    307         first = False 
    308         result += "%s = '%s'" % (fieldName, product) 
    309     return result 
     382def makeWhereClause(fieldName, values, negative=False): 
     383    if not values: 
     384        return '' 
     385    if negative: 
     386        connector, op = ' AND ', '!=' 
     387    else: 
     388        connector, op = ' OR ', '=' 
     389    clause = connector.join(["%s %s '%s'" % (fieldName, op, value) 
     390                             for value in values]) 
     391    return ' WHERE '+clause 
    310392 
    311393def convert(_db, _host, _user, _password, _env, _force): 
    312394    activityFields = FieldTranslator() 
    313395 
    314396    # account for older versions of bugzilla 
    315397    if BZ_VERSION == '2.11': 
    316         print 'Using Buzvilla v%s schema.' % BZ_VERSION 
     398        print 'Using Bugzilla v%s schema.' % BZ_VERSION 
    317399        activityFields['removed'] = 'oldvalue' 
    318400        activityFields['added'] = 'newvalue' 
    319401 
     
    343425 
    344426 
    345427    print 
     428    print "0. filtering products..." 
     429    sql = "SELECT product FROM products" 
     430    mysql_cur.execute(sql) 
     431    products = [] 
     432    for line in mysql_cur.fetchall(): 
     433        product = line['product'] 
     434        if PRODUCTS and product not in PRODUCTS: 
     435            continue 
     436        if product in IGNORE_PRODUCTS: 
     437            continue 
     438        products.append(product) 
     439    PRODUCTS[:] = products 
     440    print "using products", ' '.join(PRODUCTS) 
     441 
     442    print 
    346443    print "1. import severities..." 
    347     severities = (('blocker', '1'), ('critical', '2'), ('major', '3'), ('normal', '4'), 
    348         ('minor', '5'), ('trivial', '6'), ('enhancement', '7')) 
    349     trac.setSeverityList(severities) 
     444    trac.setSeverityList(SEVERITIES) 
    350445 
    351446    print 
    352447    print "2. import components..." 
    353     sql = "SELECT value, initialowner AS owner FROM components" 
    354     if PRODUCTS: 
    355        sql += " WHERE %s" % productFilter('program', PRODUCTS) 
    356     mysql_cur.execute(sql) 
    357     components = mysql_cur.fetchall() 
    358     for component in components: 
    359                 component['owner'] = trac.getLoginName(mysql_cur, component['owner']) 
    360     trac.setComponentList(components, 'value') 
     448    if not COMPONENTS_FROM_PRODUCTS: 
     449        sql = "SELECT value, initialowner AS owner FROM components" 
     450        sql += makeWhereClause('program', PRODUCTS) 
     451        mysql_cur.execute(sql) 
     452        components = mysql_cur.fetchall() 
     453        for component in components: 
     454            component['owner'] = trac.getLoginName(mysql_cur, component['owner']) 
     455        trac.setComponentList(components, 'value') 
     456    else: 
     457        sql = "SELECT program AS product, value AS comp, initialowner AS owner FROM components" 
     458        sql += makeWhereClause('program', PRODUCTS) 
     459        mysql_cur.execute(sql) 
     460        lines = mysql_cur.fetchall() 
     461        all_components = {} # product -> components 
     462        all_owners = {} # product, component -> owner 
     463        for line in lines: 
     464            product = line['product'] 
     465            comp = line['comp'] 
     466            owner = line['owner'] 
     467            all_components.setdefault(product, []).append(comp) 
     468            all_owners[(product, comp)] = owner 
     469        component_list = [] 
     470        for product, components in all_components.items(): 
     471            # find best default owner 
     472            default = None 
     473            for comp in DEFAULT_COMPONENTS: 
     474                if comp in components: 
     475                    default = comp 
     476                    break 
     477            if default is None: 
     478                default = components[0] 
     479            owner = all_owners[(product, default)] 
     480            owner_name = trac.getLoginName(mysql_cur, owner) 
     481            component_list.append({'product': product, 'owner': owner_name}) 
     482        trac.setComponentList(component_list, 'product') 
    361483 
    362484    print 
    363485    print "3. import priorities..." 
    364     priorities = (('P1', '1'), ('P2', '2'), ('P3', '3'), ('P4', '4'), ('P5', '5')) 
    365     trac.setPriorityList(priorities) 
     486    trac.setPriorityList(PRIORITIES) 
    366487 
    367488    print 
    368489    print "4. import versions..." 
    369490    sql = "SELECT DISTINCTROW value FROM versions" 
    370     if PRODUCTS: 
    371        sql += " WHERE %s" % productFilter('program', PRODUCTS) 
     491    sql += makeWhereClause('program', PRODUCTS) 
    372492    mysql_cur.execute(sql) 
    373493    versions = mysql_cur.fetchall() 
    374494    trac.setVersionList(versions, 'value') 
    375495 
    376496    print 
    377497    print "5. import milestones..." 
    378     mysql_cur.execute("SELECT value FROM milestones") 
     498    sql = "SELECT DISTINCT value FROM milestones" 
     499    sql += makeWhereClause('value', IGNORE_MILESTONES, negative=True) 
     500    mysql_cur.execute(sql) 
    379501    milestones = mysql_cur.fetchall() 
    380     if milestones[0] == '---': 
    381         trac.setMilestoneList(milestones, 'value') 
    382     else: 
    383         trac.setMilestoneList([], '') 
     502    trac.setMilestoneList(milestones, 'value') 
    384503 
    385504    print 
    386505    print '6. retrieving bugs...' 
    387506    sql = "SELECT * FROM bugs " 
    388     if PRODUCTS: 
    389        sql += " WHERE %s" % productFilter('product', PRODUCTS) 
     507    sql += makeWhereClause('product', PRODUCTS) 
    390508    sql += " ORDER BY bug_id" 
    391509    mysql_cur.execute(sql) 
    392510    bugs = mysql_cur.fetchall() 
     
    401519        ticket['id'] = bugid 
    402520        ticket['time'] = bug['creation_ts'] 
    403521        ticket['changetime'] = bug['delta_ts'] 
    404         ticket['component'] = bug['component'] 
     522        if COMPONENTS_FROM_PRODUCTS: 
     523            ticket['component'] = bug['product'] 
     524        else: 
     525            ticket['component'] = bug['component'] 
    405526        ticket['severity'] = bug['bug_severity'] 
    406527        ticket['priority'] = bug['priority'] 
    407528 
     
    413534        cc_list = [] 
    414535        for cc in cc_records: 
    415536            cc_list.append(trac.getLoginName(mysql_cur, cc['who'])) 
     537        cc_list = [cc for cc in cc_list if '@' in cc and cc not in IGNORE_CC] 
    416538        ticket['cc'] = string.join(cc_list, ', ') 
    417539 
    418540        ticket['version'] = bug['version'] 
    419541 
    420         if bug['target_milestone'] == '---': 
    421             ticket['milestone'] = '' 
    422         else: 
    423             ticket['milestone'] = bug['target_milestone'] 
     542        target_milestone = bug['target_milestone'] 
     543        if target_milestone in IGNORE_MILESTONES: 
     544            target_milestone = '' 
     545        ticket['milestone'] = target_milestone 
    424546 
    425547        bug_status = bug['bug_status'].lower() 
    426548        ticket['status'] = statusXlator[bug_status] 
     
    435557 
    436558        ticket['summary'] = bug['short_desc'] 
    437559 
    438         keywords = string.split(bug['keywords'], ' ') 
    439  
    440560        mysql_cur.execute("SELECT * FROM longdescs WHERE bug_id = %s" % bugid)  
    441561        longdescs = list(mysql_cur.fetchall()) 
    442562 
     
    464584        bugs_activity = mysql_cur.fetchall() 
    465585        resolution = '' 
    466586        ticketChanges = [] 
     587        keywords = [] 
    467588        for activity in bugs_activity: 
    468589            field_name = trac.getFieldName(mysql_cur, activity['fieldid']).lower() 
    469590             
     
    479600            if field_name == 'resolution': 
    480601                resolution = added.lower() 
    481602 
    482             keywordChange = False 
    483             oldKeywords = string.join(keywords, " ") 
     603            add_keywords = [] 
     604            remove_keywords = [] 
    484605 
    485606            # convert bugzilla field names... 
    486607            if field_name == 'bug_severity': 
     
    490611            elif field_name == 'bug_status': 
    491612                field_name = 'status' 
    492613                if removed in STATUS_KEYWORDS: 
    493                     kw = STATUS_KEYWORDS[removed] 
    494                     if kw in keywords: 
    495                         keywords.remove(kw) 
    496                     else: 
    497                         oldKeywords = string.join(keywords + [ kw ], " ") 
    498                     keywordChange = True 
     614                    remove_keywords.append(STATUS_KEYWORDS[removed]) 
    499615                if added in STATUS_KEYWORDS: 
    500                     kw = STATUS_KEYWORDS[added] 
    501                     keywords.append(kw) 
    502                     keywordChange = True 
     616                    add_keywords.append(STATUS_KEYWORDS[added]) 
    503617                added = statusXlator[added] 
    504618                removed = statusXlator[removed] 
    505619            elif field_name == 'short_desc': 
    506620                field_name = 'summary' 
    507             elif field_name == 'product': 
    508                 if removed in PRODUCT_KEYWORDS: 
    509                     kw = PRODUCT_KEYWORDS[removed] 
    510                     if kw in keywords: 
    511                         keywords.remove(kw) 
    512                     else: 
    513                         oldKeywords = string.join(keywords + [ kw ], " ") 
    514                     keywordChange = True 
    515                 if added in PRODUCT_KEYWORDS: 
    516                     kw = PRODUCT_KEYWORDS[added] 
    517                     keywords.append(kw) 
    518                     keywordChange = True 
     621            elif field_name == 'product' and COMPONENTS_FROM_PRODUCTS: 
     622                field_name = 'component' 
     623            elif ((field_name == 'product' and not COMPONENTS_FROM_PRODUCTS) or 
     624                  (field_name == 'component' and COMPONENTS_FROM_PRODUCTS)): 
     625                if MAP_ALL_KEYWORDS or removed in KEYWORDS_MAPPING: 
     626                    kw = KEYWORDS_MAPPING.get(removed, removed) 
     627                    if kw: 
     628                        remove_keywords.append(kw) 
     629                if MAP_ALL_KEYWORDS or added in KEYWORDS_MAPPING: 
     630                    kw = KEYWORDS_MAPPING.get(added, added) 
     631                    if kw: 
     632                        add_keywords.append(kw) 
     633                if field_name == 'component': 
     634                    # just keep the keyword change 
     635                    added = removed = '' 
     636            elif field_name == 'target_milestone': 
     637                field_name = 'milestone' 
     638                if added in IGNORE_MILESTONES: 
     639                    added = '' 
     640                if removed in IGNORE_MILESTONES: 
     641                    removed = '' 
    519642 
    520643            ticketChange = {} 
    521644            ticketChange['ticket'] = bugid 
     
    525648            ticketChange['oldvalue'] = removed 
    526649            ticketChange['newvalue'] = added 
    527650 
    528             if keywordChange: 
    529                 newKeywords = string.join(keywords, " ") 
    530                 ticketChangeKw = ticketChange 
    531                 ticketChangeKw['field'] = 'keywords' 
    532                 ticketChangeKw['oldvalue'] = oldKeywords 
    533                 ticketChangeKw['newvalue'] = newKeywords 
    534                 #trac.addTicketChange(ticket=bugid, time=activity['bug_when'], 
    535                 #    author=trac.getLoginName(mysql_cur, activity['who']), 
    536                 #    field='keywords', oldvalue=oldKeywords, newvalue=newKeywords) 
    537                 ticketChanges.append(ticketChangeKw) 
     651            if add_keywords or remove_keywords: 
     652                # ensure removed ones are in old 
     653                old_keywords = keywords + [kw for kw in remove_keywords if kw not in keywords] 
     654                # remove from new 
     655                keywords = [kw for kw in keywords if kw not in remove_keywords] 
     656                # add to new 
     657                keywords += [kw for kw in add_keywords if kw not in keywords] 
     658                if old_keywords != keywords: 
     659                    ticketChangeKw = ticketChange.copy() 
     660                    ticketChangeKw['field'] = 'keywords' 
     661                    ticketChangeKw['oldvalue'] = ' '.join(old_keywords) 
     662                    ticketChangeKw['newvalue'] = ' '.join(keywords) 
     663                    ticketChanges.append(ticketChangeKw) 
    538664 
    539665            if field_name in IGNORED_ACTIVITY_FIELDS: 
    540666                continue 
     
    552678                  oldChange['oldvalue'] += " " + ticketChange['oldvalue']  
    553679                  oldChange['newvalue'] += " " + ticketChange['newvalue'] 
    554680                  break 
     681              # cc sometime appear in different activities with same time 
     682              if (field_name == 'cc' 
     683                  and oldChange['time'] == ticketChange['time']): 
     684                  oldChange['newvalue'] += ", " + ticketChange['newvalue'] 
     685                  break 
    555686            else: 
    556                 #trac.addTicketChange(ticket=bugid, time=activity['bug_when'], 
    557                 #    author=trac.getLoginName(mysql_cur, activity['who']), 
    558                 #    field=field_name, oldvalue=removed, newvalue=added) 
    559687                ticketChanges.append (ticketChange) 
    560688 
    561689        for ticketChange in ticketChanges: 
     
    568696        if not ticket['resolution'] and ticket['status'] == 'closed': 
    569697            ticket['resolution'] = resolution 
    570698 
    571         if bug['bug_status'] in STATUS_KEYWORDS: 
    572             kw = STATUS_KEYWORDS[bug['bug_status']] 
    573             # may have already been added during activity import 
     699        bug_status = bug['bug_status'] 
     700        if bug_status in STATUS_KEYWORDS: 
     701            kw = STATUS_KEYWORDS[bug_status] 
    574702            if kw not in keywords: 
    575703                keywords.append(kw) 
    576704 
    577         if bug['product'] in PRODUCT_KEYWORDS: 
    578             kw = PRODUCT_KEYWORDS[bug['product']] 
    579             # may have already been added during activity import 
    580             if kw not in keywords: 
     705        product = bug['product'] 
     706        if product in KEYWORDS_MAPPING and not COMPONENTS_FROM_PRODUCTS: 
     707            kw = KEYWORDS_MAPPING.get(product, product) 
     708            if kw and kw not in keywords: 
     709                keywords.append(kw) 
     710 
     711        component = bug['component'] 
     712        if (COMPONENTS_FROM_PRODUCTS and 
     713            (MAP_ALL_KEYWORDS or component in KEYWORDS_MAPPING)): 
     714            kw = KEYWORDS_MAPPING.get(component, component) 
     715            if kw and kw not in keywords: 
    581716                keywords.append(kw) 
    582717 
    583718        mysql_cur.execute("SELECT * FROM attachments WHERE bug_id = %s" % bugid)