| 1 | #!/usr/bin/env python |
|---|
| 2 | |
|---|
| 3 | """ |
|---|
| 4 | Import a Mantis bug database into Trac |
|---|
| 5 | |
|---|
| 6 | Requires: Trac 0.8 from http://trac.edgewall.com/ |
|---|
| 7 | Python 2.3 from http://www.python.org/ |
|---|
| 8 | MySQL >= 3.23 from http://www.mysql.org/ |
|---|
| 9 | |
|---|
| 10 | Thanks: Dmitry Yusupov, author of bugzilla2trac on which this |
|---|
| 11 | script is based |
|---|
| 12 | |
|---|
| 13 | Copyright 2005, Blake Watters <blake@near-time.com> |
|---|
| 14 | """ |
|---|
| 15 | |
|---|
| 16 | ### |
|---|
| 17 | ### Conversion Settings -- edit these before running if desired |
|---|
| 18 | ### |
|---|
| 19 | |
|---|
| 20 | # Mantis version. |
|---|
| 21 | # |
|---|
| 22 | # Currently, the following Mantis versions are known to work: |
|---|
| 23 | # 2.11 |
|---|
| 24 | # |
|---|
| 25 | # If you run this script on a version not listed here and it is successful, |
|---|
| 26 | # please report it to the Trac mailing list so we can update the list. |
|---|
| 27 | MT_VERSION = '0.19.2' |
|---|
| 28 | |
|---|
| 29 | # MySQL connection parameters for the Mantis database. These can also |
|---|
| 30 | # be specified on the command line. |
|---|
| 31 | MT_DB = '' |
|---|
| 32 | MT_HOST = 'localhost' |
|---|
| 33 | MT_USER = '' |
|---|
| 34 | MT_PASSWORD = '' |
|---|
| 35 | |
|---|
| 36 | # Path to the Trac environment. |
|---|
| 37 | TRAC_ENV = '' |
|---|
| 38 | |
|---|
| 39 | # If true, all existing Trac tickets and attachments will be removed |
|---|
| 40 | # prior to import. |
|---|
| 41 | TRAC_CLEAN = False |
|---|
| 42 | |
|---|
| 43 | # Enclose imported ticket description and comments in a {{{ }}} |
|---|
| 44 | # preformat block? This formats the text in a fixed-point font. |
|---|
| 45 | PREFORMAT_COMMENTS = False |
|---|
| 46 | |
|---|
| 47 | # By default, all bugs are imported from Mantis. If you add a list |
|---|
| 48 | # of products here, only bugs from those projects will be imported. |
|---|
| 49 | PROJECTS = [2] |
|---|
| 50 | |
|---|
| 51 | # Trac doesn't have the concept of a project. Instead, this script can |
|---|
| 52 | # assign keywords in the ticket entry to represent projects. |
|---|
| 53 | # |
|---|
| 54 | # ex. PROJECT_KEYWORDS = { 'project1' : 'PROJECT_KEYWORD' } |
|---|
| 55 | PROJECT_KEYWORDS = {} |
|---|
| 56 | |
|---|
| 57 | # Bug comments that should not be imported. Each entry in list should |
|---|
| 58 | # be a regular expression. |
|---|
| 59 | IGNORE_COMMENTS = [] |
|---|
| 60 | |
|---|
| 61 | ########################################################################### |
|---|
| 62 | ### You probably don't need to change any configuration past this line. ### |
|---|
| 63 | ########################################################################### |
|---|
| 64 | |
|---|
| 65 | # Mantis status to Trac status translation map. |
|---|
| 66 | # |
|---|
| 67 | # NOTE: bug activity is translated as well, which may cause bug |
|---|
| 68 | # activity to be deleted (e.g. resolved -> closed in Mantis |
|---|
| 69 | # would translate into closed -> closed in Trac, so we just ignore the |
|---|
| 70 | # change). |
|---|
| 71 | # |
|---|
| 72 | # There is some special magic for open in the code: if there is no |
|---|
| 73 | # Mantis owner, open is mapped to 'new' instead. |
|---|
| 74 | STATUS_TRANSLATE = { |
|---|
| 75 | 10 : 'new', |
|---|
| 76 | 20 : 'assigned', |
|---|
| 77 | 30 : 'assigned', |
|---|
| 78 | 40 : 'assigned', |
|---|
| 79 | 50 : 'assigned', |
|---|
| 80 | 80 : 'closed', |
|---|
| 81 | 90 : 'closed' |
|---|
| 82 | } |
|---|
| 83 | |
|---|
| 84 | # |
|---|
| 85 | # Mantis resolution to Trac resolution translator |
|---|
| 86 | # |
|---|
| 87 | # $g_resolution_enum_string = '10:open,20:fixed,30:reopened,40:unable to duplicate,50:not fixable,60:duplicate,70:not a bug,80:suspended,90:wont fix'; |
|---|
| 88 | RESOLUTION_TRANSLATE = { |
|---|
| 89 | 10 : '', |
|---|
| 90 | 20 : 'fixed', |
|---|
| 91 | 30 : '', |
|---|
| 92 | 40 : 'worksforme', |
|---|
| 93 | 50 : 'wontfix', |
|---|
| 94 | 60 : 'duplicate', |
|---|
| 95 | 70 : 'invalid', |
|---|
| 96 | 80 : '', |
|---|
| 97 | 90 : 'wontfix' |
|---|
| 98 | } |
|---|
| 99 | |
|---|
| 100 | # |
|---|
| 101 | # Mantis severity to Trac severity translation map. |
|---|
| 102 | # |
|---|
| 103 | SEVERITY_TRANSLATE = { |
|---|
| 104 | '10' : 'enhancement', |
|---|
| 105 | '20' : 'trivial', |
|---|
| 106 | '30' : 'trivial', |
|---|
| 107 | '40' : 'trivial', |
|---|
| 108 | '50' : 'minor', |
|---|
| 109 | '60' : 'major', |
|---|
| 110 | '70' : 'critical', |
|---|
| 111 | '80' : 'blocker' |
|---|
| 112 | } |
|---|
| 113 | |
|---|
| 114 | # |
|---|
| 115 | # Mantis priority to Trac priority |
|---|
| 116 | # |
|---|
| 117 | PRIORITY_TRANSLATE = { |
|---|
| 118 | '10' : 'lowest', |
|---|
| 119 | '20' : 'low', |
|---|
| 120 | '30' : 'normal', |
|---|
| 121 | '40' : 'high', |
|---|
| 122 | '50' : 'highest', |
|---|
| 123 | '60' : 'highest' |
|---|
| 124 | } |
|---|
| 125 | |
|---|
| 126 | # Translate Mantis statuses into Trac keywords. This provides a way |
|---|
| 127 | # to retain the Mantis statuses in Trac. e.g. when a bug is marked |
|---|
| 128 | # 'verified' in Mantis it will be assigned a VERIFIED keyword. |
|---|
| 129 | STATUS_KEYWORDS = { |
|---|
| 130 | 'verified' : 'VERIFIED', |
|---|
| 131 | 'released' : 'RELEASED' |
|---|
| 132 | } |
|---|
| 133 | |
|---|
| 134 | # Some fields in Mantis do not have equivalents in Trac. Changes in |
|---|
| 135 | # fields listed here will not be imported into the ticket change history, |
|---|
| 136 | # otherwise you'd see changes for fields that don't exist in Trac. |
|---|
| 137 | IGNORED_ACTIVITY_FIELDS = ['everconfirmed'] |
|---|
| 138 | |
|---|
| 139 | ### |
|---|
| 140 | ### Script begins here |
|---|
| 141 | ### |
|---|
| 142 | |
|---|
| 143 | import os |
|---|
| 144 | import re |
|---|
| 145 | import sys |
|---|
| 146 | import string |
|---|
| 147 | import StringIO |
|---|
| 148 | |
|---|
| 149 | import MySQLdb |
|---|
| 150 | import MySQLdb.cursors |
|---|
| 151 | import trac.Environment |
|---|
| 152 | import sqlite |
|---|
| 153 | |
|---|
| 154 | if not hasattr(sys, 'setdefaultencoding'): |
|---|
| 155 | reload(sys) |
|---|
| 156 | |
|---|
| 157 | sys.setdefaultencoding('latin1') |
|---|
| 158 | |
|---|
| 159 | # simulated Attachment class for trac.add |
|---|
| 160 | class Attachment: |
|---|
| 161 | def __init__(self, name, data): |
|---|
| 162 | self.filename = name |
|---|
| 163 | self.file = StringIO.StringIO(data.tostring()) |
|---|
| 164 | |
|---|
| 165 | # simple field translation mapping. if string not in |
|---|
| 166 | # mapping, just return string, otherwise return value |
|---|
| 167 | class FieldTranslator(dict): |
|---|
| 168 | def __getitem__(self, item): |
|---|
| 169 | if not dict.has_key(self, item): |
|---|
| 170 | return item |
|---|
| 171 | |
|---|
| 172 | return dict.__getitem__(self, item) |
|---|
| 173 | |
|---|
| 174 | statusXlator = FieldTranslator(STATUS_TRANSLATE) |
|---|
| 175 | severityXlator = FieldTranslator(SEVERITY_TRANSLATE) |
|---|
| 176 | priorityXlator = FieldTranslator(PRIORITY_TRANSLATE) |
|---|
| 177 | resolutionXlator = FieldTranslator(RESOLUTION_TRANSLATE) |
|---|
| 178 | |
|---|
| 179 | class TracDatabase(object): |
|---|
| 180 | def __init__(self, path): |
|---|
| 181 | self.env = trac.Environment.Environment(path) |
|---|
| 182 | self._db = self.env.get_db_cnx() |
|---|
| 183 | self._db.autocommit = False |
|---|
| 184 | self.mysql_cur = False |
|---|
| 185 | self.loginNameCache = {} |
|---|
| 186 | self.fieldNameCache = {} |
|---|
| 187 | |
|---|
| 188 | def db(self): |
|---|
| 189 | return self._db |
|---|
| 190 | |
|---|
| 191 | def hasTickets(self): |
|---|
| 192 | c = self.db().cursor() |
|---|
| 193 | c.execute('''SELECT count(*) FROM Ticket''') |
|---|
| 194 | return int(c.fetchall()[0][0]) > 0 |
|---|
| 195 | |
|---|
| 196 | def assertNoTickets(self): |
|---|
| 197 | if self.hasTickets(): |
|---|
| 198 | raise Exception("Will not modify database with existing tickets!") |
|---|
| 199 | |
|---|
| 200 | def setSeverityList(self, s): |
|---|
| 201 | """Remove all severities, set them to `s`""" |
|---|
| 202 | self.assertNoTickets() |
|---|
| 203 | |
|---|
| 204 | c = self.db().cursor() |
|---|
| 205 | c.execute("""DELETE FROM enum WHERE type='severity'""") |
|---|
| 206 | for value, i in s: |
|---|
| 207 | print "inserting severity ", value, " ", i |
|---|
| 208 | c.execute("""INSERT INTO enum (type, name, value) VALUES (%s, %s, %s)""", |
|---|
| 209 | "severity", value.encode('utf-8'), i) |
|---|
| 210 | self.db().commit() |
|---|
| 211 | |
|---|
| 212 | def setPriorityList(self, s): |
|---|
| 213 | """Remove all priorities, set them to `s`""" |
|---|
| 214 | self.assertNoTickets() |
|---|
| 215 | |
|---|
| 216 | c = self.db().cursor() |
|---|
| 217 | c.execute("""DELETE FROM enum WHERE type='priority'""") |
|---|
| 218 | for value, i in s: |
|---|
| 219 | print "inserting priority ", value, " ", i |
|---|
| 220 | c.execute("""INSERT INTO enum (type, name, value) VALUES (%s, %s, %s)""", |
|---|
| 221 | "priority", |
|---|
| 222 | value.encode('utf-8'), |
|---|
| 223 | i) |
|---|
| 224 | self.db().commit() |
|---|
| 225 | |
|---|
| 226 | |
|---|
| 227 | def setComponentList(self, l, key): |
|---|
| 228 | """Remove all components, set them to `l`""" |
|---|
| 229 | self.assertNoTickets() |
|---|
| 230 | |
|---|
| 231 | c = self.db().cursor() |
|---|
| 232 | c.execute("""DELETE FROM component""") |
|---|
| 233 | for comp in l: |
|---|
| 234 | name = comp[key].strip() |
|---|
| 235 | print "inserting component '",name,"', owner", comp['owner'] |
|---|
| 236 | c.execute("""INSERT INTO component (name, owner) VALUES (%s, %s)""", |
|---|
| 237 | name.encode('utf-8'), comp['owner'].encode('utf-8')) |
|---|
| 238 | self.db().commit() |
|---|
| 239 | |
|---|
| 240 | def setVersionList(self, v, key): |
|---|
| 241 | """Remove all versions, set them to `v`""" |
|---|
| 242 | self.assertNoTickets() |
|---|
| 243 | |
|---|
| 244 | c = self.db().cursor() |
|---|
| 245 | c.execute("""DELETE FROM version""") |
|---|
| 246 | for vers in v: |
|---|
| 247 | print "inserting version ", vers[key] |
|---|
| 248 | c.execute("""INSERT INTO version (name) VALUES (%s)""", |
|---|
| 249 | vers[key].encode('utf-8')) |
|---|
| 250 | self.db().commit() |
|---|
| 251 | |
|---|
| 252 | def setMilestoneList(self, m, key): |
|---|
| 253 | """Remove all milestones, set them to `m`""" |
|---|
| 254 | self.assertNoTickets() |
|---|
| 255 | |
|---|
| 256 | c = self.db().cursor() |
|---|
| 257 | c.execute("""DELETE FROM milestone""") |
|---|
| 258 | for ms in m: |
|---|
| 259 | print "inserting milestone ", ms[key] |
|---|
| 260 | c.execute("""INSERT INTO milestone (name) VALUES (%s)""", |
|---|
| 261 | ms[key].encode('utf-8')) |
|---|
| 262 | self.db().commit() |
|---|
| 263 | |
|---|
| 264 | def addTicket(self, id, time, changetime, component, |
|---|
| 265 | severity, priority, owner, reporter, cc, |
|---|
| 266 | version, milestone, status, resolution, |
|---|
| 267 | summary, description, keywords): |
|---|
| 268 | c = self.db().cursor() |
|---|
| 269 | |
|---|
| 270 | desc = description.encode('utf-8') |
|---|
| 271 | |
|---|
| 272 | if PREFORMAT_COMMENTS: |
|---|
| 273 | desc = '{{{\n%s\n}}}' % desc |
|---|
| 274 | |
|---|
| 275 | print "inserting ticket %s -- %s" % (id, summary) |
|---|
| 276 | c.execute("""INSERT INTO ticket (id, time, changetime, component, |
|---|
| 277 | severity, priority, owner, reporter, cc, |
|---|
| 278 | version, milestone, status, resolution, |
|---|
| 279 | summary, description, keywords) |
|---|
| 280 | VALUES (%s, %s, %s, %s, |
|---|
| 281 | %s, %s, %s, %s, %s, |
|---|
| 282 | %s, %s, %s, %s, |
|---|
| 283 | %s, %s, %s)""", |
|---|
| 284 | id, time.strftime('%s'), changetime.strftime('%s'), component.encode('utf-8'), |
|---|
| 285 | severity.encode('utf-8'), priority.encode('utf-8'), owner, reporter, cc, |
|---|
| 286 | version, milestone.encode('utf-8'), status.lower(), resolution, |
|---|
| 287 | summary.encode('utf-8'), desc, keywords) |
|---|
| 288 | |
|---|
| 289 | self.db().commit() |
|---|
| 290 | return self.db().db.sqlite_last_insert_rowid() |
|---|
| 291 | |
|---|
| 292 | def addTicketComment(self, ticket, time, author, value): |
|---|
| 293 | comment = value.encode('utf-8') |
|---|
| 294 | |
|---|
| 295 | if PREFORMAT_COMMENTS: |
|---|
| 296 | comment = '{{{\n%s\n}}}' % comment |
|---|
| 297 | |
|---|
| 298 | c = self.db().cursor() |
|---|
| 299 | c.execute("""INSERT INTO ticket_change (ticket, time, author, field, oldvalue, newvalue) |
|---|
| 300 | VALUES (%s, %s, %s, %s, %s, %s)""", |
|---|
| 301 | ticket, time.strftime('%s'), author, 'comment', '', comment) |
|---|
| 302 | self.db().commit() |
|---|
| 303 | |
|---|
| 304 | def addTicketChange(self, ticket, time, author, field, oldvalue, newvalue): |
|---|
| 305 | c = self.db().cursor() |
|---|
| 306 | |
|---|
| 307 | if field == 'status': |
|---|
| 308 | oldvalue = statusXlator[int(oldvalue)] |
|---|
| 309 | newvalue = statusXlator[int(newvalue)] |
|---|
| 310 | |
|---|
| 311 | if field == 'severity': |
|---|
| 312 | oldvalue = severityXlator[oldvalue] |
|---|
| 313 | newvalue = severityXlator[newvalue] |
|---|
| 314 | |
|---|
| 315 | if field == 'owner': |
|---|
| 316 | if oldvalue == 0: |
|---|
| 317 | oldvalue = 'unassigned' |
|---|
| 318 | else: |
|---|
| 319 | oldvalue = self.getLoginName(self.mysql_cur, oldvalue) |
|---|
| 320 | |
|---|
| 321 | if newvalue == 0: |
|---|
| 322 | newvalue = 'unassigned' |
|---|
| 323 | else: |
|---|
| 324 | newvalue = self.getLoginName(self.mysql_cur, newvalue) |
|---|
| 325 | |
|---|
| 326 | if field == 'resolution': |
|---|
| 327 | oldvalue = resolutionXlator[oldvalue] |
|---|
| 328 | newvalue = resolutionXlator[newvalue] |
|---|
| 329 | |
|---|
| 330 | if field == 'priority': |
|---|
| 331 | oldvalue = priorityXlator[oldvalue] |
|---|
| 332 | newvalue = priorityXlator[newvalue] |
|---|
| 333 | |
|---|
| 334 | try: |
|---|
| 335 | c.execute("""INSERT INTO ticket_change (ticket, time, author, field, oldvalue, newvalue) |
|---|
| 336 | VALUES (%s, %s, %s, %s, %s, %s)""", |
|---|
| 337 | ticket, time.strftime('%s'), author, field, oldvalue.encode('utf-8'), newvalue.encode('utf-8')) |
|---|
| 338 | self.db().commit() |
|---|
| 339 | |
|---|
| 340 | except sqlite.IntegrityError, excp: |
|---|
| 341 | print 'Warning : Ticket ID ' + str(ticket) + ' ' + str(excp) |
|---|
| 342 | |
|---|
| 343 | |
|---|
| 344 | def addAttachment(self, id, attachment, description, author): |
|---|
| 345 | print 'inserting attachment for ticket %s -- %s' % (id, description) |
|---|
| 346 | attachment.filename = attachment.filename.encode('utf-8') |
|---|
| 347 | try: |
|---|
| 348 | self.env.create_attachment(self.db(), 'ticket', str(id), attachment, description.encode('utf-8'), |
|---|
| 349 | author, 'unknown') |
|---|
| 350 | |
|---|
| 351 | except trac.util.TracError, excp: |
|---|
| 352 | print 'Warning : Ticket ID ' + str(id) + ' ' + str(excp) |
|---|
| 353 | |
|---|
| 354 | def getLoginName(self, cursor, userid): |
|---|
| 355 | if userid not in self.loginNameCache: |
|---|
| 356 | cursor.execute("SELECT * FROM mantis_user_table WHERE id = %s" % userid) |
|---|
| 357 | loginName = cursor.fetchall() |
|---|
| 358 | |
|---|
| 359 | if loginName: |
|---|
| 360 | loginName = loginName[0]['username'] |
|---|
| 361 | else: |
|---|
| 362 | #print 'warning: unknown Mantis userid %i, recording as anonymous' % userid |
|---|
| 363 | loginName = 'anonymous' |
|---|
| 364 | |
|---|
| 365 | self.loginNameCache[userid] = loginName |
|---|
| 366 | |
|---|
| 367 | return self.loginNameCache[userid] |
|---|
| 368 | |
|---|
| 369 | def setMysqlCursor(self, cursor): |
|---|
| 370 | self.mysql_cur = cursor |
|---|
| 371 | |
|---|
| 372 | def getFieldName(self, cursor, fieldid): |
|---|
| 373 | if fieldid not in self.fieldNameCache: |
|---|
| 374 | cursor.execute("SELECT * FROM fielddefs WHERE fieldid = %s" % fieldid) |
|---|
| 375 | fieldName = cursor.fetchall() |
|---|
| 376 | |
|---|
| 377 | if fieldName: |
|---|
| 378 | fieldName = fieldName[0]['name'].lower() |
|---|
| 379 | else: |
|---|
| 380 | print 'warning: unknown Mantis fieldid %d, recording as unknown' % userid |
|---|
| 381 | fieldName = 'unknown' |
|---|
| 382 | |
|---|
| 383 | self.fieldNameCache[fieldid] = fieldName |
|---|
| 384 | |
|---|
| 385 | return self.fieldNameCache[fieldid] |
|---|
| 386 | |
|---|
| 387 | def projectFilter(fieldName, projects): |
|---|
| 388 | first = True |
|---|
| 389 | result = '' |
|---|
| 390 | for project in projects: |
|---|
| 391 | if not first: |
|---|
| 392 | result += " or " |
|---|
| 393 | first = False |
|---|
| 394 | result += "%s = '%s'" % (fieldName, project) |
|---|
| 395 | return result |
|---|
| 396 | |
|---|
| 397 | def convert(_db, _host, _user, _password, _env, _force): |
|---|
| 398 | # init Mantis environment |
|---|
| 399 | print "Mantis MySQL('%s':'%s':'%s':'%s'): connecting..." % (_db, _host, _user, _password) |
|---|
| 400 | mysql_con = MySQLdb.connect(host=_host, |
|---|
| 401 | user=_user, passwd=_password, db=_db, compress=1, |
|---|
| 402 | cursorclass=MySQLdb.cursors.DictCursor) |
|---|
| 403 | mysql_cur = mysql_con.cursor() |
|---|
| 404 | |
|---|
| 405 | # init Trac environment |
|---|
| 406 | print "Trac SQLite('%s'): connecting..." % (_env) |
|---|
| 407 | trac = TracDatabase(_env) |
|---|
| 408 | trac.setMysqlCursor(mysql_cur) |
|---|
| 409 | |
|---|
| 410 | # force mode... |
|---|
| 411 | if _force == 1: |
|---|
| 412 | print "cleaning all tickets..." |
|---|
| 413 | c = trac.db().cursor() |
|---|
| 414 | c.execute("""DELETE FROM ticket_change""") |
|---|
| 415 | trac.db().commit() |
|---|
| 416 | c.execute("""DELETE FROM ticket""") |
|---|
| 417 | trac.db().commit() |
|---|
| 418 | c.execute("""DELETE FROM attachment""") |
|---|
| 419 | os.system('rm -rf %s' % trac.env.get_attachments_dir()) |
|---|
| 420 | os.mkdir(trac.env.get_attachments_dir()) |
|---|
| 421 | trac.db().commit() |
|---|
| 422 | |
|---|
| 423 | |
|---|
| 424 | print |
|---|
| 425 | print "1. import severities..." |
|---|
| 426 | severities = (('blocker', '1'), ('critical', '2'), ('major', '3'), ('normal', '4'), |
|---|
| 427 | ('minor', '5'), ('trivial', '6'), ('enhancement', '7')) |
|---|
| 428 | trac.setSeverityList(severities) |
|---|
| 429 | |
|---|
| 430 | print |
|---|
| 431 | print "2. import components..." |
|---|
| 432 | sql = "SELECT category, user_id AS owner FROM mantis_project_category_table" |
|---|
| 433 | if PROJECTS: |
|---|
| 434 | sql += " WHERE %s" % projectFilter('project_id', PROJECTS) |
|---|
| 435 | mysql_cur.execute(sql) |
|---|
| 436 | components = mysql_cur.fetchall() |
|---|
| 437 | for component in components: |
|---|
| 438 | component['owner'] = trac.getLoginName(mysql_cur, component['owner']) |
|---|
| 439 | trac.setComponentList(components, 'category') |
|---|
| 440 | |
|---|
| 441 | print |
|---|
| 442 | print "3. import priorities..." |
|---|
| 443 | priorities = (('lowest', '1'), ('low', '2'), ('normal', '3'), ('high', '4'), ('highest', '5')) |
|---|
| 444 | trac.setPriorityList(priorities) |
|---|
| 445 | |
|---|
| 446 | print |
|---|
| 447 | print "4. import versions..." |
|---|
| 448 | sql = "SELECT DISTINCTROW version FROM mantis_project_version_table" |
|---|
| 449 | if PROJECTS: |
|---|
| 450 | sql += " WHERE %s" % projectFilter('project_id', PROJECTS) |
|---|
| 451 | mysql_cur.execute(sql) |
|---|
| 452 | versions = mysql_cur.fetchall() |
|---|
| 453 | trac.setVersionList(versions, 'version') |
|---|
| 454 | |
|---|
| 455 | print |
|---|
| 456 | print '5. retrieving bugs...' |
|---|
| 457 | sql = "SELECT * FROM mantis_bug_table " |
|---|
| 458 | if PROJECTS: |
|---|
| 459 | sql += " WHERE %s" % projectFilter('project_id', PROJECTS) |
|---|
| 460 | sql += " ORDER BY id" |
|---|
| 461 | mysql_cur.execute(sql) |
|---|
| 462 | bugs = mysql_cur.fetchall() |
|---|
| 463 | |
|---|
| 464 | print |
|---|
| 465 | print "6. import bugs and bug activity..." |
|---|
| 466 | for bug in bugs: |
|---|
| 467 | bugid = bug['id'] |
|---|
| 468 | |
|---|
| 469 | ticket = {} |
|---|
| 470 | keywords = [] |
|---|
| 471 | ticket['id'] = bugid |
|---|
| 472 | ticket['time'] = bug['date_submitted'] |
|---|
| 473 | ticket['changetime'] = bug['last_updated'] |
|---|
| 474 | ticket['component'] = bug['category'] |
|---|
| 475 | |
|---|
| 476 | severity = str(bug['severity']) |
|---|
| 477 | ticket['severity'] = severityXlator[severity] |
|---|
| 478 | |
|---|
| 479 | priority = str(bug['priority']) |
|---|
| 480 | ticket['priority'] = priorityXlator[priority] |
|---|
| 481 | |
|---|
| 482 | ticket['owner'] = trac.getLoginName(mysql_cur, bug['handler_id']) |
|---|
| 483 | ticket['reporter'] = trac.getLoginName(mysql_cur, bug['reporter_id']) |
|---|
| 484 | |
|---|
| 485 | mysql_cur.execute("SELECT * FROM mantis_bug_monitor_table WHERE bug_id = %s" % bugid) |
|---|
| 486 | cc_records = mysql_cur.fetchall() |
|---|
| 487 | cc_list = [] |
|---|
| 488 | for cc in cc_records: |
|---|
| 489 | cc_list.append(trac.getLoginName(mysql_cur, cc['user_id'])) |
|---|
| 490 | ticket['cc'] = string.join(cc_list, ', ') |
|---|
| 491 | |
|---|
| 492 | ticket['version'] = bug['version'] |
|---|
| 493 | ticket['milestone'] = '' |
|---|
| 494 | |
|---|
| 495 | bug_status = bug['status'] |
|---|
| 496 | ticket['status'] = statusXlator[bug_status] |
|---|
| 497 | ticket['resolution'] = resolutionXlator[bug['resolution']] |
|---|
| 498 | |
|---|
| 499 | # a bit of extra work to do open tickets |
|---|
| 500 | if bug_status == 'open': |
|---|
| 501 | if owner != '': |
|---|
| 502 | ticket['status'] = 'assigned' |
|---|
| 503 | else: |
|---|
| 504 | ticket['status'] = 'new' |
|---|
| 505 | |
|---|
| 506 | ticket['summary'] = bug['summary'] |
|---|
| 507 | |
|---|
| 508 | # TODO: Figure out keywords |
|---|
| 509 | #keywords = string.split(bug['keywords'], ' ') |
|---|
| 510 | keywords = '' |
|---|
| 511 | |
|---|
| 512 | mysql_cur.execute("SELECT * FROM mantis_bug_text_table WHERE id = %s" % bugid) |
|---|
| 513 | texts = mysql_cur.fetchone() |
|---|
| 514 | ticket['description'] = texts['description'] |
|---|
| 515 | |
|---|
| 516 | # Add the additional info and steps to reproduce as comments |
|---|
| 517 | trac.addTicketComment(bugid, |
|---|
| 518 | bug['date_submitted'], |
|---|
| 519 | trac.getLoginName(mysql_cur, bug['reporter_id']), |
|---|
| 520 | texts['steps_to_reproduce']) |
|---|
| 521 | |
|---|
| 522 | # trac.addTicketComment(bugid, |
|---|
| 523 | # bug['date_submitted'], |
|---|
| 524 | # trac.getLoginName(mysql_cur, bug['reporter_id']), |
|---|
| 525 | # texts['additional_information']) |
|---|
| 526 | |
|---|
| 527 | # Grab the bug notes as comments |
|---|
| 528 | mysql_cur.execute("SELECT * FROM mantis_bugnote_table WHERE bug_id = %s" % bugid) |
|---|
| 529 | bugnotes = mysql_cur.fetchall() |
|---|
| 530 | |
|---|
| 531 | for desc in bugnotes: |
|---|
| 532 | ignore = False |
|---|
| 533 | mysql_cur.execute("SELECT * FROM mantis_bugnote_text_table WHERE id = %s" % desc['id']) |
|---|
| 534 | text = mysql_cur.fetchone() |
|---|
| 535 | try: |
|---|
| 536 | note = str(text['note']) |
|---|
| 537 | except TypeError: |
|---|
| 538 | continue |
|---|
| 539 | |
|---|
| 540 | for comment in IGNORE_COMMENTS: |
|---|
| 541 | if re.match(comment, note): |
|---|
| 542 | ignore = True |
|---|
| 543 | |
|---|
| 544 | if ignore: continue |
|---|
| 545 | |
|---|
| 546 | trac.addTicketComment(bugid, |
|---|
| 547 | desc['date_submitted'], |
|---|
| 548 | trac.getLoginName(mysql_cur, desc['reporter_id']), |
|---|
| 549 | note) |
|---|
| 550 | |
|---|
| 551 | # Import the bug history |
|---|
| 552 | mysql_cur.execute("SELECT * FROM mantis_bug_history_table WHERE bug_id = %s ORDER BY date_modified" % bugid) |
|---|
| 553 | bugs_activity = mysql_cur.fetchall() |
|---|
| 554 | resolution = '' |
|---|
| 555 | ticketChanges = [] |
|---|
| 556 | for activity in bugs_activity: |
|---|
| 557 | field_name = activity['field_name'] |
|---|
| 558 | removed = activity['old_value'] |
|---|
| 559 | added = activity['new_value'] |
|---|
| 560 | |
|---|
| 561 | # remember most recent resolution, we need this later |
|---|
| 562 | if field_name == 'resolution': |
|---|
| 563 | resolution = resolutionXlator[added.lower()] |
|---|
| 564 | |
|---|
| 565 | keywordChange = False |
|---|
| 566 | oldKeywords = string.join(keywords, " ") |
|---|
| 567 | |
|---|
| 568 | # convert Mantis field names... |
|---|
| 569 | if field_name == 'handler_id': |
|---|
| 570 | field_name = 'owner' |
|---|
| 571 | elif field_name == 'status': |
|---|
| 572 | if removed in STATUS_KEYWORDS: |
|---|
| 573 | kw = STATUS_KEYWORDS[removed] |
|---|
| 574 | if kw in keywords: |
|---|
| 575 | keywords.remove(kw) |
|---|
| 576 | else: |
|---|
| 577 | oldKeywords = string.join(keywords + [ kw ], " ") |
|---|
| 578 | keywordChange = True |
|---|
| 579 | if added in STATUS_KEYWORDS: |
|---|
| 580 | kw = STATUS_KEYWORDS[added] |
|---|
| 581 | keywords.append(kw) |
|---|
| 582 | keywordChange = True |
|---|
| 583 | added = statusXlator[added] |
|---|
| 584 | removed = statusXlator[removed] |
|---|
| 585 | elif field_name == 'project_id': |
|---|
| 586 | if removed in PROJECT_KEYWORDS: |
|---|
| 587 | kw = PROJECT_KEYWORDS[removed] |
|---|
| 588 | if kw in keywords: |
|---|
| 589 | keywords.remove(kw) |
|---|
| 590 | else: |
|---|
| 591 | oldKeywords = string.join(keywords + [ kw ], " ") |
|---|
| 592 | keywordChange = True |
|---|
| 593 | if added in PROJECT_KEYWORDS: |
|---|
| 594 | kw = PROJECT_KEYWORDS[added] |
|---|
| 595 | keywords.append(kw) |
|---|
| 596 | keywordChange = True |
|---|
| 597 | |
|---|
| 598 | ticketChange = {} |
|---|
| 599 | ticketChange['ticket'] = bugid |
|---|
| 600 | ticketChange['time'] = activity['date_modified'] |
|---|
| 601 | ticketChange['author'] = trac.getLoginName(mysql_cur, activity['user_id']) |
|---|
| 602 | ticketChange['field'] = field_name |
|---|
| 603 | #ticketChange['oldvalue'] = removed |
|---|
| 604 | ticketChange['oldvalue'] = activity['old_value'] |
|---|
| 605 | #ticketChange['newvalue'] = added |
|---|
| 606 | ticketChange['newvalue'] = activity['new_value'] |
|---|
| 607 | |
|---|
| 608 | if keywordChange: |
|---|
| 609 | newKeywords = string.join(keywords, " ") |
|---|
| 610 | ticketChangeKw = ticketChange |
|---|
| 611 | ticketChangeKw['field'] = 'keywords' |
|---|
| 612 | ticketChangeKw['oldvalue'] = oldKeywords |
|---|
| 613 | ticketChangeKw['newvalue'] = newKeywords |
|---|
| 614 | #trac.addTicketChange(ticket=bugid, time=activity['bug_when'], |
|---|
| 615 | # author=trac.getLoginName(mysql_cur, activity['who']), |
|---|
| 616 | # field='keywords', oldvalue=oldKeywords, newvalue=newKeywords) |
|---|
| 617 | ticketChanges.append(ticketChangeKw) |
|---|
| 618 | |
|---|
| 619 | if field_name in IGNORED_ACTIVITY_FIELDS: |
|---|
| 620 | continue |
|---|
| 621 | |
|---|
| 622 | # skip changes that have no effect (think translation!) |
|---|
| 623 | if added == removed: |
|---|
| 624 | continue |
|---|
| 625 | |
|---|
| 626 | # Mantis splits large summary changes into two records |
|---|
| 627 | for oldChange in ticketChanges: |
|---|
| 628 | if (field_name == 'summary' |
|---|
| 629 | and oldChange['field'] == ticketChange['field'] |
|---|
| 630 | and oldChange['time'] == ticketChange['time'] |
|---|
| 631 | and oldChange['author'] == ticketChange['author']): |
|---|
| 632 | oldChange['oldvalue'] += " " + ticketChange['oldvalue'] |
|---|
| 633 | oldChange['newvalue'] += " " + ticketChange['newvalue'] |
|---|
| 634 | break |
|---|
| 635 | else: |
|---|
| 636 | #trac.addTicketChange(ticket=bugid, time=activity['bug_when'], |
|---|
| 637 | # author=trac.getLoginName(mysql_cur, activity['who']), |
|---|
| 638 | # field=field_name, oldvalue=removed, newvalue=added) |
|---|
| 639 | ticketChanges.append (ticketChange) |
|---|
| 640 | |
|---|
| 641 | for ticketChange in ticketChanges: |
|---|
| 642 | trac.addTicketChange (**ticketChange) |
|---|
| 643 | |
|---|
| 644 | if bug['status'] in STATUS_KEYWORDS: |
|---|
| 645 | kw = STATUS_KEYWORDS[bug['status']] |
|---|
| 646 | # may have already been added during activity import |
|---|
| 647 | if kw not in keywords: |
|---|
| 648 | keywords.append(kw) |
|---|
| 649 | |
|---|
| 650 | if bug['project_id'] in PROJECT_KEYWORDS: |
|---|
| 651 | kw = PROJECT_KEYWORDS[bug['project_id']] |
|---|
| 652 | # may have already been added during activity import |
|---|
| 653 | if kw not in keywords: |
|---|
| 654 | keywords.append(kw) |
|---|
| 655 | |
|---|
| 656 | mysql_cur.execute("SELECT * FROM mantis_bug_file_table WHERE bug_id = %s" % bugid) |
|---|
| 657 | attachments = mysql_cur.fetchall() |
|---|
| 658 | for a in attachments: |
|---|
| 659 | author = trac.getLoginName(mysql_cur, bug['reporter_id']) |
|---|
| 660 | |
|---|
| 661 | tracAttachment = Attachment(a['filename'], a['content']) |
|---|
| 662 | trac.addAttachment(bugid, tracAttachment, a['description'], author) |
|---|
| 663 | |
|---|
| 664 | ticket['keywords'] = string.join(keywords) |
|---|
| 665 | ticketid = trac.addTicket(**ticket) |
|---|
| 666 | |
|---|
| 667 | print "Success!" |
|---|
| 668 | |
|---|
| 669 | def usage(): |
|---|
| 670 | print "Mantis2trac - Imports a bug database from Mantis into Trac." |
|---|
| 671 | print |
|---|
| 672 | print "Usage: mantis2trac.py [options]" |
|---|
| 673 | print |
|---|
| 674 | print "Available Options:" |
|---|
| 675 | print " --db <MySQL dbname> - Mantis's database" |
|---|
| 676 | print " --tracenv /path/to/trac/env - full path to Trac db environment" |
|---|
| 677 | print " -h | --host <MySQL hostname> - Mantis's DNS host name" |
|---|
| 678 | print " -u | --user <MySQL username> - effective Mantis's database user" |
|---|
| 679 | print " -p | --passwd <MySQL password> - Mantis's user password" |
|---|
| 680 | print " -c | --clean - remove current Trac tickets before importing" |
|---|
| 681 | print " --help | help - this help info" |
|---|
| 682 | print |
|---|
| 683 | print "Additional configuration options can be defined directly in the script." |
|---|
| 684 | print |
|---|
| 685 | sys.exit(0) |
|---|
| 686 | |
|---|
| 687 | def main(): |
|---|
| 688 | global MT_DB, MT_HOST, MT_USER, MT_PASSWORD, TRAC_ENV, TRAC_CLEAN |
|---|
| 689 | if len (sys.argv) > 1: |
|---|
| 690 | if sys.argv[1] in ['--help','help'] or len(sys.argv) < 4: |
|---|
| 691 | usage() |
|---|
| 692 | iter = 1 |
|---|
| 693 | while iter < len(sys.argv): |
|---|
| 694 | if sys.argv[iter] in ['--db'] and iter+1 < len(sys.argv): |
|---|
| 695 | MT_DB = sys.argv[iter+1] |
|---|
| 696 | iter = iter + 1 |
|---|
| 697 | elif sys.argv[iter] in ['-h', '--host'] and iter+1 < len(sys.argv): |
|---|
| 698 | MT_HOST = sys.argv[iter+1] |
|---|
| 699 | iter = iter + 1 |
|---|
| 700 | elif sys.argv[iter] in ['-u', '--user'] and iter+1 < len(sys.argv): |
|---|
| 701 | MT_USER = sys.argv[iter+1] |
|---|
| 702 | iter = iter + 1 |
|---|
| 703 | elif sys.argv[iter] in ['-p', '--passwd'] and iter+1 < |
|---|