| 1 | #!/usr/bin/env python |
|---|
| 2 | |
|---|
| 3 | """ |
|---|
| 4 | Import Mantis bugs into a Trac database. |
|---|
| 5 | |
|---|
| 6 | Requires: Trac 0.8.X 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 | Version 1.0 (the "it works for me" release) |
|---|
| 11 | Date: August 9, 2005 |
|---|
| 12 | Author: Paul Baranowski (paul@paulbaranowski.org) |
|---|
| 13 | |
|---|
| 14 | Based on bugzilla2trac.py by these guys (thank you!): |
|---|
| 15 | Dmitry Yusupov <dmitry_yus@yahoo.com> - bugzilla2trac.py |
|---|
| 16 | Mark Rowe <mrowe@bluewire.net.nz> - original TracDatabase class |
|---|
| 17 | Bill Soudan <bill@soudan.net> - Many enhancements |
|---|
| 18 | |
|---|
| 19 | Example use: |
|---|
| 20 | python mantis2trac.py --db mantis --tracenv /usr/local/trac-projects/myproj/ --host localhost --user root --clean |
|---|
| 21 | |
|---|
| 22 | Notes: |
|---|
| 23 | - Private bugs will become public |
|---|
| 24 | - Some ticket changes will not be preserved since they have no |
|---|
| 25 | equivalents in Trac. |
|---|
| 26 | - I consider milestones and versions to be the same thing (actually, |
|---|
| 27 | I dont really care about the version, because for our project, bugs are |
|---|
| 28 | only in the 'previous version'). |
|---|
| 29 | - Importing attachments is not implemented (couldnt get it to work, |
|---|
| 30 | and we didnt have enough attachments to justify spending time on this) |
|---|
| 31 | "Clean" will not delete your existing attachments. There is code in here |
|---|
| 32 | to support adding attachments, but you will have to play with it to |
|---|
| 33 | make it work. If you search for the word "attachment" you will find |
|---|
| 34 | all the code related to this. |
|---|
| 35 | - Ticket descriptions & comments will be re-wrapped to 70 characters. |
|---|
| 36 | This may mess up your formatting for your bugs. If you dont want to do |
|---|
| 37 | this, search for textwrap.fill() and fix it. |
|---|
| 38 | - You will probably want to change "report.css" in trac to handle one more |
|---|
| 39 | level of priorities (default trac has 6 levels of priorities, while Mantis |
|---|
| 40 | has 7). When you look at your reports, the color schemes will look wrong. |
|---|
| 41 | |
|---|
| 42 | The lines that control the priority color scheme look like this: |
|---|
| 43 | #tktlist tr.color1-odd { background: #fdc; border-color: #e88; color: #a22 } |
|---|
| 44 | #tktlist tr.color1-even { background: #fed; border-color: #e99; color: #a22 } |
|---|
| 45 | |
|---|
| 46 | I added a new level 2 ("urgent") with an orange color, |
|---|
| 47 | and incremented all the rest of the levels: |
|---|
| 48 | #tktlist tr.color2-odd { background: #FFE08F; border-color: #e88; color: #a22 } |
|---|
| 49 | #tktlist tr.color2-even { background: #FFE59F; border-color: #e99; color: #a22 } |
|---|
| 50 | |
|---|
| 51 | """ |
|---|
| 52 | |
|---|
| 53 | import datetime |
|---|
| 54 | import textwrap |
|---|
| 55 | |
|---|
| 56 | ### |
|---|
| 57 | ### Conversion Settings -- edit these before running if desired |
|---|
| 58 | ### |
|---|
| 59 | |
|---|
| 60 | # Mantis version. |
|---|
| 61 | # |
|---|
| 62 | # Currently, the following mantis versions are known to work: |
|---|
| 63 | # 0.19.X |
|---|
| 64 | # |
|---|
| 65 | # If you run this script on a version not listed here and it is successful, |
|---|
| 66 | # please report it to the Trac mailing list so we can update the list. |
|---|
| 67 | MANTIS_VERSION = '0.19' |
|---|
| 68 | |
|---|
| 69 | # MySQL connection parameters for the Mantis database. These can also |
|---|
| 70 | # be specified on the command line. |
|---|
| 71 | MANTIS_DB = 'mantis' |
|---|
| 72 | MANTIS_HOST = 'localhost' |
|---|
| 73 | MANTIS_USER = 'root' |
|---|
| 74 | MANTIS_PASSWORD = '' |
|---|
| 75 | |
|---|
| 76 | # Path to the Trac environment. |
|---|
| 77 | TRAC_ENV = '' |
|---|
| 78 | |
|---|
| 79 | # If true, all existing Trac tickets will be removed |
|---|
| 80 | # prior to import. |
|---|
| 81 | TRAC_CLEAN = True |
|---|
| 82 | |
|---|
| 83 | # Enclose imported ticket description and comments in a {{{ }}} |
|---|
| 84 | # preformat block? This formats the text in a fixed-point font. |
|---|
| 85 | PREFORMAT_COMMENTS = True |
|---|
| 86 | |
|---|
| 87 | # By default, all bugs are imported from Mantis. If you add a list |
|---|
| 88 | # of products here, only bugs from those products will be imported. |
|---|
| 89 | # Warning: I have not tested this script where this field is blank! |
|---|
| 90 | PRODUCTS = [ 'campsite' ] |
|---|
| 91 | |
|---|
| 92 | # Trac doesn't have the concept of a product. Instead, this script can |
|---|
| 93 | # assign keywords in the ticket entry to represent products. |
|---|
| 94 | # |
|---|
| 95 | # ex. PRODUCT_KEYWORDS = { 'product1' : 'PRODUCT1_KEYWORD' } |
|---|
| 96 | PRODUCT_KEYWORDS = {} |
|---|
| 97 | |
|---|
| 98 | # Bug comments that should not be imported. Each entry in list should |
|---|
| 99 | # be a regular expression. |
|---|
| 100 | IGNORE_COMMENTS = [ |
|---|
| 101 | # '^Created an attachment \(id=' |
|---|
| 102 | ] |
|---|
| 103 | |
|---|
| 104 | # Ticket changes in Trac have the restriction where the |
|---|
| 105 | # bug ID, field, and time must be unique for all entries in the ticket |
|---|
| 106 | # changes table. |
|---|
| 107 | # Mantis, for unknown reasons, has fields that can change two states |
|---|
| 108 | # in under a second (e.g. "milestone":""->"1.0", "milestone":"1.0"->"2.0"). |
|---|
| 109 | # Setting this to true will attempt to fix these cases by adjusting the |
|---|
| 110 | # time for the 2nd change to be one second more than the original time. |
|---|
| 111 | # I dont know why you'd want to turn this off, but I give you the option |
|---|
| 112 | # anyhow. :) |
|---|
| 113 | TIME_ADJUSTMENT_HACK = True |
|---|
| 114 | |
|---|
| 115 | ########################################################################### |
|---|
| 116 | ### You probably don't need to change any configuration past this line. ### |
|---|
| 117 | ########################################################################### |
|---|
| 118 | |
|---|
| 119 | # Mantis status to Trac status translation map. |
|---|
| 120 | # |
|---|
| 121 | # NOTE: bug activity is translated as well, which may cause bug |
|---|
| 122 | # activity to be deleted (e.g. resolved -> closed in Mantis |
|---|
| 123 | # would translate into closed -> closed in Trac, so we just ignore the |
|---|
| 124 | # change). |
|---|
| 125 | # |
|---|
| 126 | # Possible Trac 'status' values: 'new', 'assigned', 'reopened', 'closed' |
|---|
| 127 | STATUS_TRANSLATE = { |
|---|
| 128 | 10 : 'new', # 10 == 'new' in mantis |
|---|
| 129 | 20 : 'assigned', # 20 == 'feedback' |
|---|
| 130 | 30 : 'new', # 30 == 'acknowledged' |
|---|
| 131 | 50 : 'assigned', # 50 == 'assigned' |
|---|
| 132 | 40 : 'new', # 40 == 'confirmed' |
|---|
| 133 | 80 : 'closed', # 80 == 'resolved' |
|---|
| 134 | 90 : 'closed' # 90 == 'closed' |
|---|
| 135 | } |
|---|
| 136 | |
|---|
| 137 | # Unused: |
|---|
| 138 | # Translate Mantis statuses into Trac keywords. This provides a way |
|---|
| 139 | # to retain the Mantis statuses in Trac. e.g. when a bug is marked |
|---|
| 140 | # 'verified' in Mantis it will be assigned a VERIFIED keyword. |
|---|
| 141 | ##STATUS_KEYWORDS = { |
|---|
| 142 | ## 'confirmed' : 'CONFIRMED', |
|---|
| 143 | ## 'feedback' : 'FEEDBACK', |
|---|
| 144 | ## 'acknowledged':'ACKNOWLEDGED' |
|---|
| 145 | ##} |
|---|
| 146 | |
|---|
| 147 | # Possible Trac resolutions are 'fixed', 'invalid', 'wontfix', 'duplicate', 'worksforme' |
|---|
| 148 | RESOLUTION_TRANSLATE = { |
|---|
| 149 | 10 : '', # 10 == 'open' in mantis |
|---|
| 150 | 20 : 'fixed', # 20 == 'fixed' |
|---|
| 151 | 30 : '', # 30 == 'reopened' (TODO: 'reopened' needs to be mapped to a status event) |
|---|
| 152 | 40 : 'invalid', # 40 == 'unable to duplicate' |
|---|
| 153 | 50 : 'wontfix', # 50 == 'not fixable' |
|---|
| 154 | 60 : 'duplicate', # 60 == 'duplicate' |
|---|
| 155 | 70 : 'invalid', # 70 == 'not an issue' |
|---|
| 156 | 80 : '', # 80 == 'suspended' |
|---|
| 157 | 90 : 'wontfix', # 90 == 'wont fix' |
|---|
| 158 | } |
|---|
| 159 | |
|---|
| 160 | # Mantis severities (which will also become equivalent Trac severities) |
|---|
| 161 | ##SEVERITY_LIST = (('block', '80'), |
|---|
| 162 | ## ('crash', '70'), |
|---|
| 163 | ## ('major', '60'), |
|---|
| 164 | ## ('minor', '50'), |
|---|
| 165 | ## ('tweak', '40'), |
|---|
| 166 | ## ('text', '30'), |
|---|
| 167 | ## ('trivial', '20'), |
|---|
| 168 | ## ('feature', '10')) |
|---|
| 169 | SEVERITY_LIST = (('block', '1'), |
|---|
| 170 | ('crash', '2'), |
|---|
| 171 | ('major', '3'), |
|---|
| 172 | ('minor', '4'), |
|---|
| 173 | ('tweak', '5'), |
|---|
| 174 | ('text', '6'), |
|---|
| 175 | ('trivial', '7'), |
|---|
| 176 | ('feature', '8')) |
|---|
| 177 | |
|---|
| 178 | # Translate severity numbers into their text equivalents |
|---|
| 179 | SEVERITY_TRANSLATE = { |
|---|
| 180 | 80 : 'block', |
|---|
| 181 | 70 : 'crash', |
|---|
| 182 | 60 : 'major', |
|---|
| 183 | 50 : 'minor', |
|---|
| 184 | 40 : 'tweak', |
|---|
| 185 | 30 : 'text', |
|---|
| 186 | 20 : 'trivial', |
|---|
| 187 | 10 : 'feature' |
|---|
| 188 | } |
|---|
| 189 | |
|---|
| 190 | # Mantis priorities (which will also become Trac priorities) |
|---|
| 191 | ##PRIORITY_LIST = (('immediate', '60'), |
|---|
| 192 | ## ('urgent', '50'), |
|---|
| 193 | ## ('high', '40'), |
|---|
| 194 | ## ('normal', '30'), |
|---|
| 195 | ## ('low', '20'), |
|---|
| 196 | ## ('none', '10')) |
|---|
| 197 | PRIORITY_LIST = (('immediate', '1'), |
|---|
| 198 | ('urgent', '2'), |
|---|
| 199 | ('high', '3'), |
|---|
| 200 | ('normal', '4'), |
|---|
| 201 | ('low', '5'), |
|---|
| 202 | ('none', '6')) |
|---|
| 203 | |
|---|
| 204 | # Translate priority numbers into their text equivalent |
|---|
| 205 | PRIORITY_TRANSLATE = { |
|---|
| 206 | 60 : 'immediate', |
|---|
| 207 | 50 : 'urgent', |
|---|
| 208 | 40 : 'high', |
|---|
| 209 | 30 : 'normal', |
|---|
| 210 | 20 : 'low', |
|---|
| 211 | 10 : 'none' |
|---|
| 212 | } |
|---|
| 213 | |
|---|
| 214 | |
|---|
| 215 | # Some fields in Mantis do not have equivalents in Trac. Changes in |
|---|
| 216 | # fields listed here will not be imported into the ticket change history, |
|---|
| 217 | # otherwise you'd see changes for fields that don't exist in Trac. |
|---|
| 218 | IGNORED_ACTIVITY_FIELDS = ['', 'project_id', 'reproducibility', 'view_state', 'os', 'os_build', 'duplicate_id'] |
|---|
| 219 | |
|---|
| 220 | ### |
|---|
| 221 | ### Script begins here |
|---|
| 222 | ### |
|---|
| 223 | |
|---|
| 224 | import os |
|---|
| 225 | import re |
|---|
| 226 | import sys |
|---|
| 227 | import string |
|---|
| 228 | import StringIO |
|---|
| 229 | |
|---|
| 230 | import MySQLdb |
|---|
| 231 | import MySQLdb.cursors |
|---|
| 232 | import trac.Environment |
|---|
| 233 | |
|---|
| 234 | if not hasattr(sys, 'setdefaultencoding'): |
|---|
| 235 | reload(sys) |
|---|
| 236 | |
|---|
| 237 | sys.setdefaultencoding('latin1') |
|---|
| 238 | |
|---|
| 239 | # simulated Attachment class for trac.add |
|---|
| 240 | class Attachment: |
|---|
| 241 | def __init__(self, name, data): |
|---|
| 242 | self.filename = name |
|---|
| 243 | self.file = StringIO.StringIO(data.tostring()) |
|---|
| 244 | |
|---|
| 245 | # simple field translation mapping. if string not in |
|---|
| 246 | # mapping, just return string, otherwise return value |
|---|
| 247 | class FieldTranslator(dict): |
|---|
| 248 | def __getitem__(self, item): |
|---|
| 249 | if not dict.has_key(self, item): |
|---|
| 250 | return item |
|---|
| 251 | |
|---|
| 252 | return dict.__getitem__(self, item) |
|---|
| 253 | |
|---|
| 254 | statusXlator = FieldTranslator(STATUS_TRANSLATE) |
|---|
| 255 | |
|---|
| 256 | class TracDatabase(object): |
|---|
| 257 | def __init__(self, path): |
|---|
| 258 | self.env = trac.Environment.Environment(path) |
|---|
| 259 | self._db = self.env.get_db_cnx() |
|---|
| 260 | self._db.autocommit = False |
|---|
| 261 | self.loginNameCache = {} |
|---|
| 262 | self.fieldNameCache = {} |
|---|
| 263 | |
|---|
| 264 | def db(self): |
|---|
| 265 | return self._db |
|---|
| 266 | |
|---|
| 267 | def hasTickets(self): |
|---|
| 268 | c = self.db().cursor() |
|---|
| 269 | c.execute('''SELECT count(*) FROM Ticket''') |
|---|
| 270 | return int(c.fetchall()[0][0]) > 0 |
|---|
| 271 | |
|---|
| 272 | def assertNoTickets(self): |
|---|
| 273 | if self.hasTickets(): |
|---|
| 274 | raise Exception("Will not modify database with existing tickets!") |
|---|
| 275 | |
|---|
| 276 | def setSeverityList(self, s): |
|---|
| 277 | """Remove all severities, set them to `s`""" |
|---|
| 278 | self.assertNoTickets() |
|---|
| 279 | |
|---|
| 280 | c = self.db().cursor() |
|---|
| 281 | c.execute("""DELETE FROM enum WHERE type='severity'""") |
|---|
| 282 | for value, i in s: |
|---|
| 283 | print "inserting severity ", value, " ", i |
|---|
| 284 | c.execute("""INSERT INTO enum (type, name, value) VALUES (%s, %s, %s)""", |
|---|
| 285 | "severity", value.encode('utf-8'), i) |
|---|
| 286 | self.db().commit() |
|---|
| 287 | |
|---|
| 288 | def setPriorityList(self, s): |
|---|
| 289 | """Remove all priorities, set them to `s`""" |
|---|
| 290 | self.assertNoTickets() |
|---|
| 291 | |
|---|
| 292 | c = self.db().cursor() |
|---|
| 293 | c.execute("""DELETE FROM enum WHERE type='priority'""") |
|---|
| 294 | for value, i in s: |
|---|
| 295 | print "inserting priority ", value, " ", i |
|---|
| 296 | c.execute("""INSERT INTO enum (type, name, value) VALUES (%s, %s, %s)""", |
|---|
| 297 | "priority", |
|---|
| 298 | value.encode('utf-8'), |
|---|
| 299 | i) |
|---|
| 300 | self.db().commit() |
|---|
| 301 | |
|---|
| 302 | |
|---|
| 303 | def setComponentList(self, l, key): |
|---|
| 304 | """Remove all components, set them to `l`""" |
|---|
| 305 | self.assertNoTickets() |
|---|
| 306 | |
|---|
| 307 | c = self.db().cursor() |
|---|
| 308 | c.execute("""DELETE FROM component""") |
|---|
| 309 | for comp in l: |
|---|
| 310 | print "inserting component '",comp[key],"', owner", comp['owner'] |
|---|
| 311 | c.execute("""INSERT INTO component (name, owner) VALUES (%s, %s)""", |
|---|
| 312 | comp[key].encode('utf-8'), comp['owner'].encode('utf-8')) |
|---|
| 313 | self.db().commit() |
|---|
| 314 | |
|---|
| 315 | def setVersionList(self, v, key): |
|---|
| 316 | """Remove all versions, set them to `v`""" |
|---|
| 317 | self.assertNoTickets() |
|---|
| 318 | |
|---|
| 319 | c = self.db().cursor() |
|---|
| 320 | c.execute("""DELETE FROM version""") |
|---|
| 321 | for vers in v: |
|---|
| 322 | print "inserting version ", vers[key] |
|---|
| 323 | c.execute("""INSERT INTO version (name) VALUES (%s)""", |
|---|
| 324 | vers[key].encode('utf-8')) |
|---|
| 325 | self.db().commit() |
|---|
| 326 | |
|---|
| 327 | def setMilestoneList(self, m, key): |
|---|
| 328 | """Remove all milestones, set them to `m`""" |
|---|
| 329 | self.assertNoTickets() |
|---|
| 330 | |
|---|
| 331 | c = self.db().cursor() |
|---|
| 332 | c.execute("""DELETE FROM milestone""") |
|---|
| 333 | for ms in m: |
|---|
| 334 | print "inserting milestone ", ms[key] |
|---|
| 335 | c.execute("""INSERT INTO milestone (name) VALUES (%s)""", |
|---|
| 336 | ms[key].encode('utf-8')) |
|---|
| 337 | self.db().commit() |
|---|
| 338 | |
|---|
| 339 | def addTicket(self, id, time, changetime, component, |
|---|
| 340 | severity, priority, owner, reporter, cc, |
|---|
| 341 | version, milestone, status, resolution, |
|---|
| 342 | summary, description, keywords): |
|---|
| 343 | c = self.db().cursor() |
|---|
| 344 | |
|---|
| 345 | description = textwrap.fill(description, 70) |
|---|
| 346 | desc = description.encode('utf-8') |
|---|
| 347 | |
|---|
| 348 | if PREFORMAT_COMMENTS: |
|---|
| 349 | desc = '{{{\n%s\n}}}' % desc |
|---|
| 350 | |
|---|
| 351 | print "inserting ticket %s -- \"%s\"" % (id, summary[0:40].replace("\n", " ")) |
|---|
| 352 | c.execute("""INSERT INTO ticket (id, time, changetime, component, |
|---|
| 353 | severity, priority, owner, reporter, cc, |
|---|
| 354 | version, milestone, status, resolution, |
|---|
| 355 | summary, description, keywords) |
|---|
| 356 | VALUES (%s, %s, %s, %s, |
|---|
| 357 | %s, %s, %s, %s, %s, |
|---|
| 358 | %s, %s, %s, %s, |
|---|
| 359 | %s, %s, %s)""", |
|---|
| 360 | id, time.strftime('%s'), changetime.strftime('%s'), component.encode('utf-8'), |
|---|
| 361 | severity.encode('utf-8'), priority.encode('utf-8'), owner, reporter, cc, |
|---|
| 362 | version, milestone.encode('utf-8'), status.lower(), resolution, |
|---|
| 363 | summary.encode('utf-8'), desc, keywords) |
|---|
| 364 | |
|---|
| 365 | self.db().commit() |
|---|
| 366 | return self.db().db.sqlite_last_insert_rowid() |
|---|
| 367 | |
|---|
| 368 | def addTicketComment(self, ticket, time, author, value): |
|---|
| 369 | print " * adding comment \"%s...\"" % value[0:40] |
|---|
| 370 | comment = textwrap.fill(value, 70) |
|---|
| 371 | comment = comment.encode('utf-8') |
|---|
| 372 | |
|---|
| 373 | if PREFORMAT_COMMENTS: |
|---|
| 374 | comment = '{{{\n%s\n}}}' % comment |
|---|
| 375 | |
|---|
| 376 | c = self.db().cursor() |
|---|
| 377 | c.execute("""INSERT INTO ticket_change (ticket, time, author, field, oldvalue, newvalue) |
|---|
| 378 | VALUES (%s, %s, %s, %s, %s, %s)""", |
|---|
| 379 | ticket, time.strftime('%s'), author, 'comment', '', comment) |
|---|
| 380 | self.db().commit() |
|---|
| 381 | |
|---|
| 382 | def addTicketChange(self, ticket, time, author, field, oldvalue, newvalue): |
|---|
| 383 | print " * adding ticket change \"%s\": \"%s\" -> \"%s\" (%s)" % (field, oldvalue[0:20], newvalue[0:20], time) |
|---|
| 384 | c = self.db().cursor() |
|---|
| 385 | c.execute("""INSERT INTO ticket_change (ticket, time, author, field, oldvalue, newvalue) |
|---|
| 386 | VALUES (%s, %s, %s, %s, %s, %s)""", |
|---|
| 387 | ticket, time.strftime('%s'), author, field, oldvalue.encode('utf-8'), newvalue.encode('utf-8')) |
|---|
| 388 | self.db().commit() |
|---|
| 389 | # Now actually change the ticket because the ticket wont update itself! |
|---|
| 390 | sql = "UPDATE ticket SET %s='%s' WHERE id=%s" % (field, newvalue, ticket) |
|---|
| 391 | c.execute(sql) |
|---|
| 392 | self.db().commit() |
|---|
| 393 | |
|---|
| 394 | def addAttachment(self, id, attachment, description, author): |
|---|
| 395 | print 'inserting attachment for ticket %s -- %s' % (id, description) |
|---|
| 396 | attachment.filename = attachment.filename.encode('utf-8') |
|---|
| 397 | self.env.create_attachment(self.db(), 'ticket', str(id), attachment, description.encode('utf-8'), |
|---|
| 398 | author, 'unknown') |
|---|
| 399 | |
|---|
| 400 | def getLoginName(self, cursor, userid): |
|---|
| 401 | if userid not in self.loginNameCache: |
|---|
| 402 | cursor.execute("SELECT * FROM mantis_user_table WHERE id = %s" % userid) |
|---|
| 403 | loginName = cursor.fetchall() |
|---|
| 404 | |
|---|
| 405 | if loginName: |
|---|
| 406 | loginName = loginName[0]['username'] |
|---|
| 407 | else: |
|---|
| 408 | print 'warning: unknown mantis userid %d, recording as anonymous' % userid |
|---|
| 409 | loginName = 'anonymous' |
|---|
| 410 | |
|---|
| 411 | self.loginNameCache[userid] = loginName |
|---|
| 412 | |
|---|
| 413 | return self.loginNameCache[userid] |
|---|
| 414 | |
|---|
| 415 | |
|---|
| 416 | def productFilter(fieldName, products): |
|---|
| 417 | first = True |
|---|
| 418 | result = '' |
|---|
| 419 | for product in products: |
|---|
| 420 | if not first: |
|---|
| 421 | result += " or " |
|---|
| 422 | first = False |
|---|
| 423 | result += "%s = '%s'" % (fieldName, product) |
|---|
| 424 | return result |
|---|
| 425 | |
|---|
| 426 | def convert(_db, _host, _user, _password, _env, _force): |
|---|
| 427 | activityFields = FieldTranslator() |
|---|
| 428 | |
|---|
| 429 | # account for older versions of mantis |
|---|
| 430 | if MANTIS_VERSION == '0.19': |
|---|
| 431 | print 'Using Mantis v%s schema.' % MANTIS_VERSION |
|---|
| 432 | activityFields['removed'] = 'oldvalue' |
|---|
| 433 | activityFields['added'] = 'newvalue' |
|---|
| 434 | |
|---|
| 435 | # init Mantis environment |
|---|
| 436 | print "Mantis MySQL('%s':'%s':'%s':'%s'): connecting..." % (_db, _host, _user, _password) |
|---|
| 437 | mysql_con = MySQLdb.connect(host=_host, |
|---|
| 438 | user=_user, passwd=_password, db=_db, compress=1, |
|---|
| 439 | cursorclass=MySQLdb.cursors.DictCursor) |
|---|
| 440 | mysql_cur = mysql_con.cursor() |
|---|
| 441 | |
|---|
| 442 | # init Trac environment |
|---|
| 443 | print "Trac SQLite('%s'): connecting..." % (_env) |
|---|
| 444 | trac = TracDatabase(_env) |
|---|
| 445 | |
|---|
| 446 | # force mode... |
|---|
| 447 | if _force == 1: |
|---|
| 448 | print "cleaning all tickets..." |
|---|
| 449 | c = trac.db().cursor() |
|---|
| 450 | c.execute("""DELETE FROM ticket_change""") |
|---|
| 451 | trac.db().commit() |
|---|
| 452 | c.execute("""DELETE FROM ticket""") |
|---|
| 453 | trac.db().commit() |
|---|
| 454 | ## c.execute("""DELETE FROM attachment""") |
|---|
| 455 | ## os.system('rm -rf %s' % trac.env.get_attachments_dir()) |
|---|
| 456 | ## os.mkdir(trac.env.get_attachments_dir()) |
|---|
| 457 | ## trac.db().commit() |
|---|
| 458 | |
|---|
| 459 | print |
|---|
| 460 | print '0. Finding project IDs...' |
|---|
| 461 | sql = "SELECT id, name FROM mantis_project_table WHERE %s" % productFilter('name', PRODUCTS) |
|---|
| 462 | mysql_cur.execute(sql) |
|---|
| 463 | project_list = mysql_cur.fetchall() |
|---|
| 464 | project_dict = dict() |
|---|
| 465 | for project_id in project_list: |
|---|
| 466 | print "Mantis project name '%s' has project ID %s" % (project_id['name'], project_id['id']) |
|---|
| 467 | project_dict[project_id['id']] = project_id['id'] |
|---|
| 468 | |
|---|
| 469 | print |
|---|
| 470 | print "1. import severities..." |
|---|
| 471 | trac.setSeverityList(SEVERITY_LIST) |
|---|
| 472 | |
|---|
| 473 | print |
|---|
| 474 | print "2. import components..." |
|---|
| 475 | sql = "SELECT category, user_id as owner FROM mantis_project_category_table" |
|---|
| 476 | if PRODUCTS: |
|---|
| 477 | sql += " WHERE %s" % productFilter('project_id', project_dict) |
|---|
| 478 | mysql_cur.execute(sql) |
|---|
| 479 | components = mysql_cur.fetchall() |
|---|
| 480 | for component in components: |
|---|
| 481 | component['owner'] = trac.getLoginName(mysql_cur, component['owner']) |
|---|
| 482 | trac.setComponentList(components, 'category') |
|---|
| 483 | |
|---|
| 484 | print |
|---|
| 485 | print "3. import priorities..." |
|---|
| 486 | trac.setPriorityList(PRIORITY_LIST) |
|---|
| 487 | |
|---|
| 488 | print |
|---|
| 489 | print "4. import versions..." |
|---|
| 490 | sql = "SELECT DISTINCTROW version FROM mantis_project_version_table" |
|---|
| 491 | if PRODUCTS: |
|---|
| 492 | sql += " WHERE %s" % productFilter('project_id', project_dict) |
|---|
| 493 | mysql_cur.execute(sql) |
|---|
| 494 | versions = mysql_cur.fetchall() |
|---|
| 495 | trac.setVersionList(versions, 'version') |
|---|
| 496 | |
|---|
| 497 | print |
|---|
| 498 | print "5. import milestones..." |
|---|
| 499 | sql = "SELECT version FROM mantis_project_version_table" |
|---|
| 500 | if PRODUCTS: |
|---|
| 501 | sql += " WHERE %s" % productFilter('project_id', project_dict) |
|---|
| 502 | mysql_cur.execute(sql) |
|---|
| 503 | milestones = mysql_cur.fetchall() |
|---|
| 504 | trac.setMilestoneList(milestones, 'version') |
|---|
| 505 | |
|---|
| 506 | print |
|---|
| 507 | print '6. retrieving bugs...' |
|---|
| 508 | sql = "SELECT * FROM mantis_bug_table " |
|---|
| 509 | if PRODUCTS: |
|---|
| 510 | sql += " WHERE %s" % productFilter('project_id', project_dict) |
|---|
| 511 | sql += " ORDER BY id" |
|---|
| 512 | mysql_cur.execute(sql) |
|---|
| 513 | bugs = mysql_cur.fetchall() |
|---|
| 514 | |
|---|
| 515 | print |
|---|
| 516 | print "7. import bugs and bug activity..." |
|---|
| 517 | totalComments = 0 |
|---|
| 518 | totalTicketChanges = 0 |
|---|
| 519 | ## totalAttachments = 0 |
|---|
| 520 | errors = [] |
|---|
| 521 | timeAdjustmentHacks = [] |
|---|
| 522 | for bug in bugs: |
|---|
| 523 | bugid = bug['id'] |
|---|
| 524 | |
|---|
| 525 | ticket = {} |
|---|
| 526 | keywords = [] |
|---|
| 527 | ticket['id'] = bugid |
|---|
| 528 | ticket['time'] = bug['date_submitted'] |
|---|
| 529 | ticket['changetime'] = bug['last_updated'] |
|---|
| 530 | ticket['component'] = bug['category'] |
|---|
| 531 | ticket['severity'] = SEVERITY_TRANSLATE[bug['severity']] |
|---|
| 532 | ticket['priority'] = PRIORITY_TRANSLATE[bug['priority']] |
|---|
| 533 | ticket['owner'] = trac.getLoginName(mysql_cur, bug['handler_id']) |
|---|
| 534 | ticket['reporter'] = trac.getLoginName(mysql_cur, bug['reporter_id']) |
|---|
| 535 | ticket['version'] = bug['version'] |
|---|
| 536 | ticket['milestone'] = bug['version'] |
|---|
| 537 | ticket['summary'] = bug['summary'] |
|---|
| 538 | ticket['status'] = STATUS_TRANSLATE[bug['status']] |
|---|
| 539 | ticket['cc'] = '' |
|---|
| 540 | ticket['keywords'] = '' |
|---|
| 541 | |
|---|
| 542 | # Special case for 'reopened' resolution in mantis - |
|---|
| 543 | # it maps to a status type in Trac. |
|---|
| 544 | if (bug['resolution'] == 30): |
|---|
| 545 | ticket['status'] = 'reopened' |
|---|
| 546 | ticket['resolution'] = RESOLUTION_TRANSLATE[bug['resolution']] |
|---|
| 547 | |
|---|
| 548 | # Compose the description from the three text fields in Mantis: |
|---|
| 549 | # 'description', 'steps_to_reproduce', 'additional_information' |
|---|
| 550 | mysql_cur.execute("SELECT * FROM mantis_bug_text_table WHERE id = %s" % bugid) |
|---|
| 551 | longdescs = list(mysql_cur.fetchall()) |
|---|
| 552 | |
|---|
| 553 | # check for empty 'longdescs[0]' field... |
|---|
| 554 | if len(longdescs) == 0: |
|---|
| 555 | ticket['description'] = '' |
|---|
| 556 | else: |
|---|
| 557 | tmpDescr = longdescs[0]['description'] |
|---|
| 558 | if (longdescs[0]['steps_to_reproduce'].strip() != ''): |
|---|
| 559 | tmpDescr = ('%s\n\nSTEPS TO REPRODUCE:\n%s') % (tmpDescr, longdescs[0]['steps_to_reproduce']) |
|---|
| 560 | if (longdescs[0]['additional_information'].strip() != ''): |
|---|
| 561 | tmpDescr = ('%s\n\nADDITIONAL INFORMATION:\n%s') % (tmpDescr, longdescs[0]['additional_information']) |
|---|
| 562 | ticket['description'] = tmpDescr |
|---|
| 563 | del longdescs[0] |
|---|
| 564 | |
|---|
| 565 | # Add the ticket to the Trac database |
|---|
| 566 | trac.addTicket(**ticket) |
|---|
| 567 | |
|---|
| 568 | # |
|---|
| 569 | # Add ticket comments |
|---|
| 570 | # |
|---|
| 571 | mysql_cur.execute("SELECT * FROM mantis_bugnote_table, mantis_bugnote_text_table WHERE bug_id = %s AND mantis_bugnote_table.id = mantis_bugnote_text_table.id ORDER BY date_submitted" % bugid) |
|---|
| 572 | bug_notes = mysql_cur.fetchall() |
|---|
| 573 | totalComments += len(bug_notes) |
|---|
| 574 | for note in bug_notes: |
|---|
| 575 | trac.addTicketComment(bugid, note['date_submitted'], trac.getLoginName(mysql_cur, note['reporter_id']), note['note']) |
|---|
| 576 | |
|---|
| 577 | # |
|---|
| 578 | # Convert ticket changes |
|---|
| 579 | # |
|---|
| 580 | mysql_cur.execute("SELECT * FROM mantis_bug_history_table WHERE bug_id = %s ORDER BY date_modified" % bugid) |
|---|
| 581 | bugs_activity = mysql_cur.fetchall() |
|---|
| 582 | resolution = '' |
|---|
| 583 | ticketChanges = [] |
|---|
| 584 | for activity in bugs_activity: |
|---|
| 585 | field_name = activity['field_name'].lower() |
|---|
| 586 | # Convert Mantis field names... |
|---|
| 587 | # The following fields are the same in Mantis and Trac: |
|---|
| 588 | # - 'status' |
|---|
| 589 | # - 'priority' |
|---|
| 590 | # - 'summary' |
|---|
| 591 | # - 'resolution' |
|---|
| 592 | # - 'severity' |
|---|
| 593 | # - 'version' |
|---|
| 594 | # |
|---|
| 595 | # Ignore the following changes: |
|---|
| 596 | # - project_id |
|---|
| 597 | # - reproducibility |
|---|
| 598 | # - view_state |
|---|
| 599 | # - os |
|---|
| 600 | # - os_build |
|---|
| 601 | # - duplicate_id |
|---|
| 602 | # |
|---|
| 603 | # Convert Mantis -> Trac: |
|---|
| 604 | # - 'handler_id' -> 'owner' |
|---|
| 605 | # - 'fixed_in_version' -> 'milestone' |
|---|
| 606 | # - 'category' -> 'component' |
|---|
| 607 | # - 'version' -> 'milestone' |
|---|
| 608 | |
|---|
| 609 | ticketChange = {} |
|---|
| 610 | ticketChange['ticket'] = bugid |
|---|
| 611 | ticketChange['oldvalue'] = activity['old_value'] |
|---|
| 612 | ticketChange['newvalue'] = activity['new_value'] |
|---|
| 613 | ticketChange['time'] = activity['date_modified'] |
|---|
| 614 | ticketChange['author'] = trac.getLoginName(mysql_cur, activity['user_id']) |
|---|
| 615 | ticketChange['field'] = field_name |
|---|
| 616 | |
|---|
| 617 | if field_name == 'handler_id': |
|---|
| 618 | ticketChange['field'] = 'owner' |
|---|
| 619 | ticketChange['oldvalue'] = trac.getLoginName(mysql_cur, int(activity['old_value'])) |
|---|
| 620 | ticketChange['newvalue'] = trac.getLoginName(mysql_cur, int(activity['new_value'])) |
|---|
| 621 | elif field_name == 'fixed_in_version': |
|---|
| 622 | ticketChange['field'] = 'milestone' |
|---|
| 623 | elif field_name == 'category': |
|---|
| 624 | ticketChange['field'] = 'component' |
|---|
| 625 | elif field_name == 'version': |
|---|
| 626 | ticketChange['field'] = 'milestone' |
|---|
| 627 | elif field_name == 'status': |
|---|
| 628 | ticketChange['oldvalue'] = STATUS_TRANSLATE[int(activity['old_value'])] |
|---|
| 629 | ticketChange['newvalue'] = STATUS_TRANSLATE[int(activity['new_value'])] |
|---|
| 630 | elif field_name == 'priority': |
|---|
| 631 | ticketChange['oldvalue'] = PRIORITY_TRANSLATE[int(activity['old_value'])] |
|---|
| 632 | ticketChange['newvalue'] = PRIORITY_TRANSLATE[int(activity['new_value'])] |
|---|
| 633 | elif field_name == 'resolution': |
|---|
| 634 | ticketChange['oldvalue'] = RESOLUTION_TRANSLATE[int(activity['old_value'])] |
|---|
| 635 | ticketChange['newvalue'] = RESOLUTION_TRANSLATE[int(activity['new_value'])] |
|---|
| 636 | elif field_name == 'severity': |
|---|
| 637 | ticketChange['oldvalue'] = SEVERITY_TRANSLATE[int(activity['old_value'])] |
|---|
| 638 | ticketChange['newvalue'] = SEVERITY_TRANSLATE[int(activity['new_value'])] |
|---|
| 639 | |
|---|
| 640 | if field_name in IGNORED_ACTIVITY_FIELDS: |
|---|
| 641 | continue |
|---|
| 642 | |
|---|
| 643 | # skip changes that have no effect (think translation!) |
|---|
| 644 | if ticketChange['oldvalue'] == ticketChange['newvalue']: |
|---|
| 645 | continue |
|---|
| 646 | |
|---|
| 647 | ticketChanges.append (ticketChange) |
|---|
| 648 | |
|---|
| 649 | totalTicketChanges += len(ticketChanges) |
|---|
| 650 | for ticketChange in ticketChanges: |
|---|
| 651 | try: |
|---|
| 652 | trac.addTicketChange (**ticketChange) |
|---|
| 653 | except: |
|---|
| 654 | if TIME_ADJUSTMENT_HACK: |
|---|
| 655 | addTime = datetime.timedelta(seconds=1) |
|---|
| 656 | originalTime = ticketChange['time'] |
|---|
| 657 | ticketChange['time'] += addTime |
|---|
| 658 | try: |
|---|
| 659 | trac.addTicketChange(**ticketChange) |
|---|
| 660 | noticeStr = " ~ Successfully adjusted time for ticket(#%s) change \"%s\": \"%s\" -> \"%s\" (%s)" % (bugid, ticketChange['field'], ticketChange['oldvalue'], ticketChange['newvalue'], ticketChange['time']) |
|---|
| 661 | noticeStr += "\n Original time: %s" % originalTime |
|---|
| 662 | timeAdjustmentHacks.append(noticeStr) |
|---|
| 663 | except: |
|---|
| 664 | errorStr = " * ERROR: unable to add ticket(#%s) change \"%s\": \"%s\" -> \"%s\" (%s)" % (bugid, ticketChange['field'], ticketChange['oldvalue'], ticketChange['newvalue'], ticketChange['time']) |
|---|
| 665 | errorStr += "\n The bug id, field name, and time must be unique" |
|---|
| 666 | errors.append(errorStr) |
|---|
| 667 | print errorStr |
|---|
| 668 | else: |
|---|
| 669 | errorStr = " * ERROR: unable to add ticket(#%s) change \"%s\": \"%s\" -> \"%s\" (%s)" % (bugid, ticketChange['field'], ticketChange['oldvalue'], ticketChange['newvalue'], ticketChange['time']) |
|---|
| 670 | errorStr += "\n The bug id, field name, and time must be unique" |
|---|
| 671 | errors.append(errorStr) |
|---|
| 672 | print errorStr |
|---|
| 673 | |
|---|
| 674 | |
|---|
| 675 | # |
|---|
| 676 | # Add ticket file attachments |
|---|
| 677 | # |
|---|
| 678 | ## mysql_cur.execute("SELECT * FROM mantis_bug_file_table WHERE bug_id = %s" % bugid) |
|---|
| 679 | ## attachments = mysql_cur.fetchall() |
|---|
| 680 | ## for attachment in attachments: |
|---|
| 681 | ## author = '' |
|---|
| 682 | ## try: |
|---|
| 683 | ## attachmentFile = open(attachment['diskfile'], 'r') |
|---|
| 684 | ## attachmentData = attachmentFile.read() |
|---|
| 685 | ## tracAttachment = Attachment(attachment['filename'], attachmentData) |
|---|
| 686 | ## trac.addAttachment(bugid, tracAttachment, attachment['description'], author) |
|---|
| 687 | ## totalAttachments += 1 |
|---|
| 688 | ## except: |
|---|
| 689 | ## errorStr = " * ERROR: couldnt find attachment %s" % attachment['diskfile'] |
|---|
| 690 | |
|---|