Ticket #1462: bugzilla2trac.py.diff
| File bugzilla2trac.py.diff, 19.2 KB (added by Florent Guillaume <fg@…>, 4 years ago) |
|---|
-
bugzilla2trac.py
old new 13 13 Copyright 2004, Dmitry Yusupov <dmitry_yus@yahoo.com> 14 14 15 15 Many enhancements, Bill Soudan <bill@soudan.net> 16 Other enhancements, Florent Guillaume <fg@nuxeo.com> 16 17 """ 17 18 19 import re 20 18 21 ### 19 22 ### Conversion Settings -- edit these before running if desired 20 23 ### … … 22 25 # Bugzilla version. You can find this in Bugzilla's globals.pl file. 23 26 # 24 27 # Currently, the following bugzilla versions are known to work: 25 # 2.11 28 # 2.11, 2.16.5 26 29 # 27 30 # If you run this script on a version not listed here and it is successful, 28 31 # please report it to the Trac mailing list so we can update the list. … … 30 33 31 34 # MySQL connection parameters for the Bugzilla database. These can also 32 35 # be specified on the command line. 33 BZ_DB = ' '36 BZ_DB = 'bugs' 34 37 BZ_HOST = 'localhost' 35 38 BZ_USER = '' 36 39 BZ_PASSWORD = '' … … 46 49 # preformat block? This formats the text in a fixed-point font. 47 50 PREFORMAT_COMMENTS = False 48 51 52 # Replace bug numbers in comments with #xyz 53 REPLACE_BUG_NO = False 54 55 # Severities 56 SEVERITIES = [ 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 67 PRIORITIES = [ 68 ('P1', '1'), 69 ('P2', '2'), 70 ('P3', '3'), 71 ('P4', '4'), 72 ('P5', '5'), 73 ] 74 49 75 # By default, all bugs are imported from Bugzilla. If you add a list 50 76 # of products here, only bugs from those products will be imported. 51 77 PRODUCTS = [] 78 # These Bugzilla products will be ignored during import. 79 IGNORE_PRODUCTS = [] 80 81 # These milestones are ignored 82 IGNORE_MILESTONES = ['---'] 83 84 # These logins are converted to these user ids 85 LOGIN_MAP = { 86 #'some.user@example.com': 'someuser', 87 } 88 89 # These emails are removed from CC list 90 IGNORE_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 103 COMPONENTS_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. 107 DEFAULT_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. 112 KEYWORDS_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. 120 MAP_ALL_KEYWORDS = True 52 121 53 # Trac doesn't have the concept of a product. Instead, this script can54 # assign keywords in the ticket entry to represent products.55 #56 # ex. PRODUCT_KEYWORDS = { 'product1' : 'PRODUCT1_KEYWORD' }57 PRODUCT_KEYWORDS = {}58 122 59 123 # Bug comments that should not be imported. Each entry in list should 60 124 # be a regular expression. … … 96 160 # otherwise you'd see changes for fields that don't exist in Trac. 97 161 IGNORED_ACTIVITY_FIELDS = ['everconfirmed'] 98 162 163 # Regular expression and its replacement 164 BUG_NO_RE = re.compile(r'\b(bug #?)([0-9])') 165 BUG_NO_REPL = r'#\2' 166 99 167 ### 100 168 ### Script begins here 101 169 ### 102 170 103 171 import os 104 import re105 172 import sys 106 173 import string 107 174 import StringIO 108 175 109 176 import MySQLdb 110 177 import MySQLdb.cursors 111 import trac.env 178 try: 179 from trac.env import Environment 180 except: 181 from trac.Environment import Environment 112 182 113 183 if not hasattr(sys, 'setdefaultencoding'): 114 184 reload(sys) … … 134 204 135 205 class TracDatabase(object): 136 206 def __init__(self, path): 137 self.env = trac.env.Environment(path)207 self.env = Environment(path) 138 208 self._db = self.env.get_db_cnx() 139 209 self._db.autocommit = False 140 210 self.loginNameCache = {} … … 210 280 c = self.db().cursor() 211 281 c.execute("""DELETE FROM milestone""") 212 282 for ms in m: 213 print "inserting milestone ", ms[key] 283 milestone = ms[key] 284 print "inserting milestone ", milestone 214 285 c.execute("""INSERT INTO milestone (name) VALUES (%s)""", 215 m s[key].encode('utf-8'))286 milestone.encode('utf-8')) 216 287 self.db().commit() 217 288 218 289 def addTicket(self, id, time, changetime, component, … … 226 297 if PREFORMAT_COMMENTS: 227 298 desc = '{{{\n%s\n}}}' % desc 228 299 300 if REPLACE_BUG_NO: 301 if BUG_NO_RE.search(desc): 302 desc = re.sub(BUG_NO_RE, BUG_NO_REPL, desc) 303 229 304 print "inserting ticket %s -- %s" % (id, summary) 230 305 c.execute("""INSERT INTO ticket (id, time, changetime, component, 231 306 severity, priority, owner, reporter, cc, … … 249 324 if PREFORMAT_COMMENTS: 250 325 comment = '{{{\n%s\n}}}' % comment 251 326 327 if REPLACE_BUG_NO: 328 if BUG_NO_RE.search(comment): 329 comment = re.sub(BUG_NO_RE, BUG_NO_REPL, comment) 330 252 331 c = self.db().cursor() 253 332 c.execute("""INSERT INTO ticket_change (ticket, time, author, field, oldvalue, newvalue) 254 333 VALUES (%s, %s, %s, %s, %s, %s)""", … … 279 358 print 'warning: unknown bugzilla userid %d, recording as anonymous' % userid 280 359 loginName = 'anonymous' 281 360 361 loginName = LOGIN_MAP.get(loginName, loginName) 362 282 363 self.loginNameCache[userid] = loginName 283 364 284 365 return self.loginNameCache[userid] … … 298 379 299 380 return self.fieldNameCache[fieldid] 300 381 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 382 def 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 310 392 311 393 def convert(_db, _host, _user, _password, _env, _force): 312 394 activityFields = FieldTranslator() 313 395 314 396 # account for older versions of bugzilla 315 397 if BZ_VERSION == '2.11': 316 print 'Using Bu zvilla v%s schema.' % BZ_VERSION398 print 'Using Bugzilla v%s schema.' % BZ_VERSION 317 399 activityFields['removed'] = 'oldvalue' 318 400 activityFields['added'] = 'newvalue' 319 401 … … 343 425 344 426 345 427 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 346 443 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) 350 445 351 446 print 352 447 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') 361 483 362 484 print 363 485 print "3. import priorities..." 364 priorities = (('P1', '1'), ('P2', '2'), ('P3', '3'), ('P4', '4'), ('P5', '5')) 365 trac.setPriorityList(priorities) 486 trac.setPriorityList(PRIORITIES) 366 487 367 488 print 368 489 print "4. import versions..." 369 490 sql = "SELECT DISTINCTROW value FROM versions" 370 if PRODUCTS: 371 sql += " WHERE %s" % productFilter('program', PRODUCTS) 491 sql += makeWhereClause('program', PRODUCTS) 372 492 mysql_cur.execute(sql) 373 493 versions = mysql_cur.fetchall() 374 494 trac.setVersionList(versions, 'value') 375 495 376 496 print 377 497 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) 379 501 milestones = mysql_cur.fetchall() 380 if milestones[0] == '---': 381 trac.setMilestoneList(milestones, 'value') 382 else: 383 trac.setMilestoneList([], '') 502 trac.setMilestoneList(milestones, 'value') 384 503 385 504 print 386 505 print '6. retrieving bugs...' 387 506 sql = "SELECT * FROM bugs " 388 if PRODUCTS: 389 sql += " WHERE %s" % productFilter('product', PRODUCTS) 507 sql += makeWhereClause('product', PRODUCTS) 390 508 sql += " ORDER BY bug_id" 391 509 mysql_cur.execute(sql) 392 510 bugs = mysql_cur.fetchall() … … 401 519 ticket['id'] = bugid 402 520 ticket['time'] = bug['creation_ts'] 403 521 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'] 405 526 ticket['severity'] = bug['bug_severity'] 406 527 ticket['priority'] = bug['priority'] 407 528 … … 413 534 cc_list = [] 414 535 for cc in cc_records: 415 536 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] 416 538 ticket['cc'] = string.join(cc_list, ', ') 417 539 418 540 ticket['version'] = bug['version'] 419 541 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 424 546 425 547 bug_status = bug['bug_status'].lower() 426 548 ticket['status'] = statusXlator[bug_status] … … 435 557 436 558 ticket['summary'] = bug['short_desc'] 437 559 438 keywords = string.split(bug['keywords'], ' ')439 440 560 mysql_cur.execute("SELECT * FROM longdescs WHERE bug_id = %s" % bugid) 441 561 longdescs = list(mysql_cur.fetchall()) 442 562 … … 464 584 bugs_activity = mysql_cur.fetchall() 465 585 resolution = '' 466 586 ticketChanges = [] 587 keywords = [] 467 588 for activity in bugs_activity: 468 589 field_name = trac.getFieldName(mysql_cur, activity['fieldid']).lower() 469 590 … … 479 600 if field_name == 'resolution': 480 601 resolution = added.lower() 481 602 482 keywordChange = False483 oldKeywords = string.join(keywords, " ")603 add_keywords = [] 604 remove_keywords = [] 484 605 485 606 # convert bugzilla field names... 486 607 if field_name == 'bug_severity': … … 490 611 elif field_name == 'bug_status': 491 612 field_name = 'status' 492 613 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]) 499 615 if added in STATUS_KEYWORDS: 500 kw = STATUS_KEYWORDS[added] 501 keywords.append(kw) 502 keywordChange = True 616 add_keywords.append(STATUS_KEYWORDS[added]) 503 617 added = statusXlator[added] 504 618 removed = statusXlator[removed] 505 619 elif field_name == 'short_desc': 506 620 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 = '' 519 642 520 643 ticketChange = {} 521 644 ticketChange['ticket'] = bugid … … 525 648 ticketChange['oldvalue'] = removed 526 649 ticketChange['newvalue'] = added 527 650 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) 538 664 539 665 if field_name in IGNORED_ACTIVITY_FIELDS: 540 666 continue … … 552 678 oldChange['oldvalue'] += " " + ticketChange['oldvalue'] 553 679 oldChange['newvalue'] += " " + ticketChange['newvalue'] 554 680 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 555 686 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)559 687 ticketChanges.append (ticketChange) 560 688 561 689 for ticketChange in ticketChanges: … … 568 696 if not ticket['resolution'] and ticket['status'] == 'closed': 569 697 ticket['resolution'] = resolution 570 698 571 if bug['bug_status'] in STATUS_KEYWORDS:572 kw = STATUS_KEYWORDS[bug['bug_status']]573 # may have already been added during activity import699 bug_status = bug['bug_status'] 700 if bug_status in STATUS_KEYWORDS: 701 kw = STATUS_KEYWORDS[bug_status] 574 702 if kw not in keywords: 575 703 keywords.append(kw) 576 704 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: 581 716 keywords.append(kw) 582 717 583 718 mysql_cur.execute("SELECT * FROM attachments WHERE bug_id = %s" % bugid)
