Ticket #1462: bugzilla2trac.py.4.diff
| File bugzilla2trac.py.4.diff, 22.2 KB (added by charlie.clark@…, 3 years ago) |
|---|
-
bugzilla2trac.py
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. … … 31 34 # MySQL connection parameters for the Bugzilla database. These can also 32 35 # be specified on the command line. 33 36 BZ_DB = '' 34 BZ_HOST = ' localhost'37 BZ_HOST = '' 35 38 BZ_USER = '' 36 39 BZ_PASSWORD = '' 37 40 … … 40 43 41 44 # If true, all existing Trac tickets and attachments will be removed 42 45 # prior to import. 43 TRAC_CLEAN = F alse46 TRAC_CLEAN = FALSE 44 47 45 48 # Enclose imported ticket description and comments in a {{{ }}} 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 = [] 52 80 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 = {} 81 # These milestones are ignored 82 IGNORE_MILESTONES = ['---'] 58 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 121 122 59 123 # Bug comments that should not be imported. Each entry in list should 60 124 # be a regular expression. 61 125 IGNORE_COMMENTS = [ … … 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 183 from trac import attachment 184 113 185 if not hasattr(sys, 'setdefaultencoding'): 114 186 reload(sys) 115 187 116 188 sys.setdefaultencoding('latin1') 117 189 118 # simulated Attachment class for trac.add 119 class Attachment: 120 def __init__(self, name, data): 121 self.filename = name 122 self.file = StringIO.StringIO(data.tostring()) 190 123 191 124 192 # simple field translation mapping. if string not in 125 193 # mapping, just return string, otherwise return value … … 134 202 135 203 class TracDatabase(object): 136 204 def __init__(self, path): 137 self.env = trac.env.Environment(path)205 self.env = Environment(path) 138 206 self._db = self.env.get_db_cnx() 139 207 self._db.autocommit = False 140 208 self.loginNameCache = {} … … 210 278 c = self.db().cursor() 211 279 c.execute("""DELETE FROM milestone""") 212 280 for ms in m: 213 print "inserting milestone ", ms[key] 281 milestone = ms[key] 282 print "inserting milestone ", milestone 214 283 c.execute("""INSERT INTO milestone (name) VALUES (%s)""", 215 m s[key].encode('utf-8'))284 milestone.encode('utf-8')) 216 285 self.db().commit() 217 286 218 287 def addTicket(self, id, time, changetime, component, … … 226 295 if PREFORMAT_COMMENTS: 227 296 desc = '{{{\n%s\n}}}' % desc 228 297 298 if REPLACE_BUG_NO: 299 if BUG_NO_RE.search(desc): 300 desc = re.sub(BUG_NO_RE, BUG_NO_REPL, desc) 301 229 302 print "inserting ticket %s -- %s" % (id, summary) 230 303 c.execute("""INSERT INTO ticket (id, time, changetime, component, 231 304 severity, priority, owner, reporter, cc, … … 249 322 if PREFORMAT_COMMENTS: 250 323 comment = '{{{\n%s\n}}}' % comment 251 324 325 if REPLACE_BUG_NO: 326 if BUG_NO_RE.search(comment): 327 comment = re.sub(BUG_NO_RE, BUG_NO_REPL, comment) 328 252 329 c = self.db().cursor() 253 330 c.execute("""INSERT INTO ticket_change (ticket, time, author, field, oldvalue, newvalue) 254 331 VALUES (%s, %s, %s, %s, %s, %s)""", … … 261 338 VALUES (%s, %s, %s, %s, %s, %s)""", 262 339 ticket, time.strftime('%s'), author, field, oldvalue.encode('utf-8'), newvalue.encode('utf-8')) 263 340 self.db().commit() 341 264 342 265 def addAttachment(self, id, attachment, description, author):266 print 'inserting attachment for ticket %s -- %s' % (id, description)267 attachment.filename = attachment.filename.encode('utf-8')268 self.env.create_attachment(self.db(), 'ticket', str(id), attachment, description.encode('utf-8'),269 author, 'unknown')270 271 343 def getLoginName(self, cursor, userid): 272 344 if userid not in self.loginNameCache: 273 345 cursor.execute("SELECT * FROM profiles WHERE userid = %s" % userid) … … 279 351 print 'warning: unknown bugzilla userid %d, recording as anonymous' % userid 280 352 loginName = 'anonymous' 281 353 354 loginName = LOGIN_MAP.get(loginName, loginName) 355 282 356 self.loginNameCache[userid] = loginName 283 357 284 358 return self.loginNameCache[userid] … … 298 372 299 373 return self.fieldNameCache[fieldid] 300 374 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 375 def makeWhereClause(fieldName, values, negative=False): 376 if not values: 377 return '' 378 if negative: 379 connector, op = ' AND ', '!=' 380 else: 381 connector, op = ' OR ', '=' 382 clause = connector.join(["%s %s '%s'" % (fieldName, op, value) 383 for value in values]) 384 return ' WHERE '+clause 310 385 311 386 def convert(_db, _host, _user, _password, _env, _force): 312 387 activityFields = FieldTranslator() 313 388 314 389 # account for older versions of bugzilla 315 390 if BZ_VERSION == '2.11': 316 print 'Using Bu zvilla v%s schema.' % BZ_VERSION391 print 'Using Bugzilla v%s schema.' % BZ_VERSION 317 392 activityFields['removed'] = 'oldvalue' 318 393 activityFields['added'] = 'newvalue' 319 394 … … 337 412 c.execute("""DELETE FROM ticket""") 338 413 trac.db().commit() 339 414 c.execute("""DELETE FROM attachment""") 340 os.system('rm -rf %s' % trac.env.get_attachments_dir()) 341 os.mkdir(trac.env.get_attachments_dir()) 415 attachments_dir = os.path.join(os.path.normpath(trac.env.path), 'attachments') 416 os.system('rm -rf %s' % attachments_dir) 417 os.mkdir(attachments_dir) 342 418 trac.db().commit() 343 419 344 420 345 421 print 422 print "0. filtering products..." 423 sql = "SELECT product FROM products" 424 mysql_cur.execute(sql) 425 products = [] 426 for line in mysql_cur.fetchall(): 427 product = line['product'] 428 if PRODUCTS and product not in PRODUCTS: 429 continue 430 if product in IGNORE_PRODUCTS: 431 continue 432 products.append(product) 433 PRODUCTS[:] = products 434 print "using products", ' '.join(PRODUCTS) 435 436 print 346 437 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) 438 trac.setSeverityList(SEVERITIES) 350 439 351 440 print 352 441 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') 442 if not COMPONENTS_FROM_PRODUCTS: 443 sql = "SELECT value, initialowner AS owner FROM components" 444 sql += makeWhereClause('program', PRODUCTS) 445 mysql_cur.execute(sql) 446 components = mysql_cur.fetchall() 447 for component in components: 448 component['owner'] = trac.getLoginName(mysql_cur, component['owner']) 449 trac.setComponentList(components, 'value') 450 else: 451 sql = "SELECT program AS product, value AS comp, initialowner AS owner FROM components" 452 sql += makeWhereClause('program', PRODUCTS) 453 mysql_cur.execute(sql) 454 lines = mysql_cur.fetchall() 455 all_components = {} # product -> components 456 all_owners = {} # product, component -> owner 457 for line in lines: 458 product = line['product'] 459 comp = line['comp'] 460 owner = line['owner'] 461 all_components.setdefault(product, []).append(comp) 462 all_owners[(product, comp)] = owner 463 component_list = [] 464 for product, components in all_components.items(): 465 # find best default owner 466 default = None 467 for comp in DEFAULT_COMPONENTS: 468 if comp in components: 469 default = comp 470 break 471 if default is None: 472 default = components[0] 473 owner = all_owners[(product, default)] 474 owner_name = trac.getLoginName(mysql_cur, owner) 475 component_list.append({'product': product, 'owner': owner_name}) 476 trac.setComponentList(component_list, 'product') 361 477 362 478 print 363 479 print "3. import priorities..." 364 priorities = (('P1', '1'), ('P2', '2'), ('P3', '3'), ('P4', '4'), ('P5', '5')) 365 trac.setPriorityList(priorities) 480 trac.setPriorityList(PRIORITIES) 366 481 367 482 print 368 483 print "4. import versions..." 369 484 sql = "SELECT DISTINCTROW value FROM versions" 370 if PRODUCTS: 371 sql += " WHERE %s" % productFilter('program', PRODUCTS) 485 sql += makeWhereClause('program', PRODUCTS) 372 486 mysql_cur.execute(sql) 373 487 versions = mysql_cur.fetchall() 374 488 trac.setVersionList(versions, 'value') 375 489 376 490 print 377 491 print "5. import milestones..." 378 mysql_cur.execute("SELECT value FROM milestones") 492 sql = "SELECT DISTINCT value FROM milestones" 493 sql += makeWhereClause('value', IGNORE_MILESTONES, negative=True) 494 mysql_cur.execute(sql) 379 495 milestones = mysql_cur.fetchall() 380 if milestones[0] == '---': 381 trac.setMilestoneList(milestones, 'value') 382 else: 383 trac.setMilestoneList([], '') 496 trac.setMilestoneList(milestones, 'value') 384 497 385 498 print 386 499 print '6. retrieving bugs...' 387 500 sql = "SELECT * FROM bugs " 388 if PRODUCTS: 389 sql += " WHERE %s" % productFilter('product', PRODUCTS) 501 sql += makeWhereClause('product', PRODUCTS) 390 502 sql += " ORDER BY bug_id" 391 503 mysql_cur.execute(sql) 392 504 bugs = mysql_cur.fetchall() … … 401 513 ticket['id'] = bugid 402 514 ticket['time'] = bug['creation_ts'] 403 515 ticket['changetime'] = bug['delta_ts'] 404 ticket['component'] = bug['component'] 516 if COMPONENTS_FROM_PRODUCTS: 517 ticket['component'] = bug['product'] 518 else: 519 ticket['component'] = bug['component'] 405 520 ticket['severity'] = bug['bug_severity'] 406 521 ticket['priority'] = bug['priority'] 407 522 … … 413 528 cc_list = [] 414 529 for cc in cc_records: 415 530 cc_list.append(trac.getLoginName(mysql_cur, cc['who'])) 531 cc_list = [cc for cc in cc_list if '@' in cc and cc not in IGNORE_CC] 416 532 ticket['cc'] = string.join(cc_list, ', ') 417 533 418 534 ticket['version'] = bug['version'] 419 535 420 if bug['target_milestone'] == '---':421 ticket['milestone'] = ''422 else:423 ticket['milestone'] = bug['target_milestone']536 target_milestone = bug['target_milestone'] 537 if target_milestone in IGNORE_MILESTONES: 538 target_milestone = '' 539 ticket['milestone'] = target_milestone 424 540 425 541 bug_status = bug['bug_status'].lower() 426 542 ticket['status'] = statusXlator[bug_status] … … 435 551 436 552 ticket['summary'] = bug['short_desc'] 437 553 438 keywords = string.split(bug['keywords'], ' ')439 440 554 mysql_cur.execute("SELECT * FROM longdescs WHERE bug_id = %s" % bugid) 441 555 longdescs = list(mysql_cur.fetchall()) 442 556 … … 464 578 bugs_activity = mysql_cur.fetchall() 465 579 resolution = '' 466 580 ticketChanges = [] 581 keywords = [] 467 582 for activity in bugs_activity: 468 583 field_name = trac.getFieldName(mysql_cur, activity['fieldid']).lower() 469 584 … … 479 594 if field_name == 'resolution': 480 595 resolution = added.lower() 481 596 482 keywordChange = False483 oldKeywords = string.join(keywords, " ")597 add_keywords = [] 598 remove_keywords = [] 484 599 485 600 # convert bugzilla field names... 486 601 if field_name == 'bug_severity': … … 490 605 elif field_name == 'bug_status': 491 606 field_name = 'status' 492 607 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 608 remove_keywords.append(STATUS_KEYWORDS[removed]) 499 609 if added in STATUS_KEYWORDS: 500 kw = STATUS_KEYWORDS[added] 501 keywords.append(kw) 502 keywordChange = True 610 add_keywords.append(STATUS_KEYWORDS[added]) 503 611 added = statusXlator[added] 504 612 removed = statusXlator[removed] 505 613 elif field_name == 'short_desc': 506 614 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 615 elif field_name == 'product' and COMPONENTS_FROM_PRODUCTS: 616 field_name = 'component' 617 elif ((field_name == 'product' and not COMPONENTS_FROM_PRODUCTS) or 618 (field_name == 'component' and COMPONENTS_FROM_PRODUCTS)): 619 if MAP_ALL_KEYWORDS or removed in KEYWORDS_MAPPING: 620 kw = KEYWORDS_MAPPING.get(removed, removed) 621 if kw: 622 remove_keywords.append(kw) 623 if MAP_ALL_KEYWORDS or added in KEYWORDS_MAPPING: 624 kw = KEYWORDS_MAPPING.get(added, added) 625 if kw: 626 add_keywords.append(kw) 627 if field_name == 'component': 628 # just keep the keyword change 629 added = removed = '' 630 elif field_name == 'target_milestone': 631 field_name = 'milestone' 632 if added in IGNORE_MILESTONES: 633 added = '' 634 if removed in IGNORE_MILESTONES: 635 removed = '' 519 636 520 637 ticketChange = {} 521 638 ticketChange['ticket'] = bugid … … 525 642 ticketChange['oldvalue'] = removed 526 643 ticketChange['newvalue'] = added 527 644 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) 645 if add_keywords or remove_keywords: 646 # ensure removed ones are in old 647 old_keywords = keywords + [kw for kw in remove_keywords if kw not in keywords] 648 # remove from new 649 keywords = [kw for kw in keywords if kw not in remove_keywords] 650 # add to new 651 keywords += [kw for kw in add_keywords if kw not in keywords] 652 if old_keywords != keywords: 653 ticketChangeKw = ticketChange.copy() 654 ticketChangeKw['field'] = 'keywords' 655 ticketChangeKw['oldvalue'] = ' '.join(old_keywords) 656 ticketChangeKw['newvalue'] = ' '.join(keywords) 657 ticketChanges.append(ticketChangeKw) 538 658 539 659 if field_name in IGNORED_ACTIVITY_FIELDS: 540 660 continue … … 552 672 oldChange['oldvalue'] += " " + ticketChange['oldvalue'] 553 673 oldChange['newvalue'] += " " + ticketChange['newvalue'] 554 674 break 675 # cc sometime appear in different activities with same time 676 if (field_name == 'cc' 677 and oldChange['time'] == ticketChange['time']): 678 oldChange['newvalue'] += ", " + ticketChange['newvalue'] 679 break 555 680 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 681 ticketChanges.append (ticketChange) 560 682 561 683 for ticketChange in ticketChanges: … … 568 690 if not ticket['resolution'] and ticket['status'] == 'closed': 569 691 ticket['resolution'] = resolution 570 692 571 if bug['bug_status'] in STATUS_KEYWORDS:572 kw = STATUS_KEYWORDS[bug['bug_status']]573 # may have already been added during activity import693 bug_status = bug['bug_status'] 694 if bug_status in STATUS_KEYWORDS: 695 kw = STATUS_KEYWORDS[bug_status] 574 696 if kw not in keywords: 575 697 keywords.append(kw) 576 698 577 if bug['product'] in PRODUCT_KEYWORDS:578 kw = PRODUCT_KEYWORDS[bug['product']]579 # may have already been added during activity import580 if kw not in keywords:699 product = bug['product'] 700 if product in KEYWORDS_MAPPING and not COMPONENTS_FROM_PRODUCTS: 701 kw = KEYWORDS_MAPPING.get(product, product) 702 if kw and kw not in keywords: 581 703 keywords.append(kw) 582 704 705 component = bug['component'] 706 if (COMPONENTS_FROM_PRODUCTS and 707 (MAP_ALL_KEYWORDS or component in KEYWORDS_MAPPING)): 708 kw = KEYWORDS_MAPPING.get(component, component) 709 if kw and kw not in keywords: 710 keywords.append(kw) 711 583 712 mysql_cur.execute("SELECT * FROM attachments WHERE bug_id = %s" % bugid) 584 713 attachments = mysql_cur.fetchall() 585 714 for a in attachments: 586 715 author = trac.getLoginName(mysql_cur, a['submitter_id']) 587 716 588 tracAttachment = Attachment(a['filename'], a['thedata']) 589 trac.addAttachment(bugid, tracAttachment, a['description'], author) 717 #tracAttachment = Attachment(a['filename'], a['thedata']) 718 #trac.addAttachment(bugid, tracAttachment, a['description'], author) 719 #self.env.create_attachment(self.db(), 'ticket', str(id), attachment, description.encode('utf-8'), 720 721 tracAttachment = attachment.Attachment(trac.env, 'ticket', str(a['bug_id']), None, trac.db()) 722 tracAttachment.insert(a['filename'], StringIO.StringIO(a['thedata'].tostring()), len(a['thedata'])) 590 723 591 724 ticket['keywords'] = string.join(keywords) 592 725 ticketid = trac.addTicket(**ticket) … … 639 772 print "Error: unknown parameter: " + sys.argv[iter] 640 773 sys.exit(0) 641 774 iter = iter + 1 642 else:643 usage()644 775 645 776 convert(BZ_DB, BZ_HOST, BZ_USER, BZ_PASSWORD, TRAC_ENV, TRAC_CLEAN) 646 777
