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