Edgewall Software

TracImport: mantis2trac.2.py

File mantis2trac.2.py, 29.9 kB (added by jpm@…, 2 years ago)

Version 1.1 of mantis2trac.py script that works against Trac 0.9.3

Line 
1#!/usr/bin/env python
2
3"""
4Import Mantis bugs into a Trac database.
5
6Requires:  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
10Version 1.1
11Date: January 23, 2006
12Author: Joao Prado Maia (jpm@pessoal.org)
13
14Based on version 1.0 from:
15Paul Baranowski (paul@paulbaranowski.org)
16
17Based on bugzilla2trac.py by these guys (thank you!):
18Dmitry Yusupov <dmitry_yus@yahoo.com> - bugzilla2trac.py
19Mark Rowe <mrowe@bluewire.net.nz> - original TracDatabase class
20Bill Soudan <bill@soudan.net> - Many enhancements
21
22Example use:
23  python mantis2trac.py --db mantis --tracenv /usr/local/trac-projects/myproj/ --host localhost --user root --clean
24
25Changes since version 1.0:
26  - Made it to work against Trac 0.9.3 (tweaks to make the Environment class work)
27  - Re-did all prepared statements-like queries to avoid a DB error
28  - Fixed a reference to the wrong variable name when adding a comment
29
30Notes:
31  - Private bugs will become public
32  - Some ticket changes will not be preserved since they have no
33    equivalents in Trac.
34  - I consider milestones and versions to be the same thing (actually,
35    I dont really care about the version, because for our project, bugs are
36    only in the 'previous version').
37  - Importing attachments is not implemented (couldnt get it to work,
38    and we didnt have enough attachments to justify spending time on this)
39    "Clean" will not delete your existing attachments.  There is code in here
40    to support adding attachments, but you will have to play with it to
41    make it work.  If you search for the word "attachment" you will find
42    all the code related to this.
43  - Ticket descriptions & comments will be re-wrapped to 70 characters.
44    This may mess up your formatting for your bugs.  If you dont want to do
45    this, search for textwrap.fill() and fix it.
46  - You will probably want to change "report.css" in trac to handle one more
47    level of priorities (default trac has 6 levels of priorities, while Mantis
48    has 7).  When you look at your reports, the color schemes will look wrong.
49   
50    The lines that control the priority color scheme look like this:
51    #tktlist tr.color1-odd  { background: #fdc; border-color: #e88; color: #a22 }
52    #tktlist tr.color1-even { background: #fed; border-color: #e99; color: #a22 }
53   
54    I added a new level 2 ("urgent") with an orange color,
55    and incremented all the rest of the levels:
56    #tktlist tr.color2-odd  { background: #FFE08F; border-color: #e88; color: #a22 }
57    #tktlist tr.color2-even { background: #FFE59F; border-color: #e99; color: #a22 }
58   
59"""
60
61import datetime
62
63###
64### Conversion Settings -- edit these before running if desired
65###
66
67# Mantis version. 
68#
69# Currently, the following mantis versions are known to work:
70#   0.19.X
71#
72# If you run this script on a version not listed here and it is successful,
73# please report it to the Trac mailing list so we can update the list.
74MANTIS_VERSION = '0.19'
75
76# MySQL connection parameters for the Mantis database.  These can also
77# be specified on the command line.
78MANTIS_DB = 'mantis'
79MANTIS_HOST = 'localhost'
80MANTIS_USER = 'root'
81MANTIS_PASSWORD = ''
82
83# Path to the Trac environment.
84TRAC_ENV = ''
85
86# If true, all existing Trac tickets will be removed
87# prior to import.
88TRAC_CLEAN = True
89
90# Enclose imported ticket description and comments in a {{{ }}}
91# preformat block?  This formats the text in a fixed-point font.
92PREFORMAT_COMMENTS = True
93
94# By default, all bugs are imported from Mantis.  If you add a list
95# of products here, only bugs from those products will be imported.
96# Warning: I have not tested this script where this field is blank!
97PRODUCTS = [ 'Web Interface' ]
98
99# Trac doesn't have the concept of a product.  Instead, this script can
100# assign keywords in the ticket entry to represent products.
101#
102# ex. PRODUCT_KEYWORDS = { 'product1' : 'PRODUCT1_KEYWORD' }
103PRODUCT_KEYWORDS = {}
104
105# Bug comments that should not be imported.  Each entry in list should
106# be a regular expression.
107IGNORE_COMMENTS = [
108#   '^Created an attachment \(id='
109]
110
111# Ticket changes in Trac have the restriction where the
112# bug ID, field, and time must be unique for all entries in the ticket
113# changes table.
114# Mantis, for unknown reasons, has fields that can change two states
115# in under a second (e.g. "milestone":""->"1.0", "milestone":"1.0"->"2.0").
116# Setting this to true will attempt to fix these cases by adjusting the
117# time for the 2nd change to be one second more than the original time.
118# I dont know why you'd want to turn this off, but I give you the option
119# anyhow. :)
120TIME_ADJUSTMENT_HACK = True
121
122###########################################################################
123### You probably don't need to change any configuration past this line. ###
124###########################################################################
125
126# Mantis status to Trac status translation map.
127#
128# NOTE: bug activity is translated as well, which may cause bug
129# activity to be deleted (e.g. resolved -> closed in Mantis
130# would translate into closed -> closed in Trac, so we just ignore the
131# change).
132#
133# Possible Trac 'status' values: 'new', 'assigned', 'reopened', 'closed'
134STATUS_TRANSLATE = {
135  10 : 'new',      # 10 == 'new' in mantis
136  20 : 'assigned', # 20 == 'feedback'
137  30 : 'new',      # 30 == 'acknowledged'
138  50 : 'assigned', # 50 == 'assigned'
139  40 : 'new',      # 40 == 'confirmed'
140  80 : 'closed',   # 80 == 'resolved'
141  90 : 'closed'    # 90 == 'closed'
142}
143
144# Unused:
145# Translate Mantis statuses into Trac keywords.  This provides a way
146# to retain the Mantis statuses in Trac.  e.g. when a bug is marked
147# 'verified' in Mantis it will be assigned a VERIFIED keyword.
148##STATUS_KEYWORDS = {
149##    'confirmed' : 'CONFIRMED',
150##    'feedback' : 'FEEDBACK',
151##    'acknowledged':'ACKNOWLEDGED'
152##}
153
154# Possible Trac resolutions are 'fixed', 'invalid', 'wontfix', 'duplicate', 'worksforme'
155RESOLUTION_TRANSLATE = {
156    10 : '',          # 10 == 'open' in mantis
157    20 : 'fixed',     # 20 == 'fixed'
158    30 : '',          # 30 == 'reopened' (TODO: 'reopened' needs to be mapped to a status event)
159    40 : 'invalid',   # 40 == 'unable to duplicate'
160    50 : 'wontfix',   # 50 == 'not fixable'
161    60 : 'duplicate', # 60 == 'duplicate'
162    70 : 'invalid',   # 70 == 'not an issue'
163    80 : '',          # 80 == 'suspended'
164    90 : 'wontfix',   # 90 == 'wont fix'
165}
166
167# Mantis severities (which will also become equivalent Trac severities)
168##SEVERITY_LIST = (('block', '80'),
169##                 ('crash', '70'),
170##                 ('major', '60'),
171##                 ('minor', '50'),
172##                 ('tweak', '40'),
173##                 ('text', '30'),
174##                 ('trivial', '20'),
175##                 ('feature', '10'))
176SEVERITY_LIST = (('block', '1'), 
177                 ('crash', '2'), 
178                 ('major', '3'), 
179                 ('minor', '4'),
180                 ('tweak', '5'), 
181                 ('text', '6'), 
182                 ('trivial', '7'), 
183                 ('feature', '8'))
184
185# Translate severity numbers into their text equivalents
186SEVERITY_TRANSLATE = {
187    80 : 'block',
188    70 : 'crash',
189    60 : 'major',
190    50 : 'minor',
191    40 : 'tweak',
192    30 : 'text',
193    20 : 'trivial',
194    10 : 'feature'
195}
196
197# Mantis priorities (which will also become Trac priorities)
198##PRIORITY_LIST = (('immediate', '60'),
199##                 ('urgent', '50'),
200##                 ('high', '40'),
201##                 ('normal', '30'),
202##                 ('low', '20'),
203##                 ('none', '10'))
204PRIORITY_LIST = (('immediate', '1'), 
205                 ('urgent', '2'), 
206                 ('high', '3'), 
207                 ('normal', '4'), 
208                 ('low', '5'), 
209                 ('none', '6'))
210
211# Translate priority numbers into their text equivalent
212PRIORITY_TRANSLATE = {
213    60 : 'immediate', 
214    50 : 'urgent', 
215    40 : 'high',
216    30 : 'normal', 
217    20 : 'low', 
218    10 : 'none'
219}
220
221
222# Some fields in Mantis do not have equivalents in Trac.  Changes in
223# fields listed here will not be imported into the ticket change history,
224# otherwise you'd see changes for fields that don't exist in Trac.
225IGNORED_ACTIVITY_FIELDS = ['', 'project_id', 'reproducibility', 'view_state', 'os', 'os_build', 'duplicate_id']
226
227###
228### Script begins here
229###
230
231import os
232import re
233import sys
234import string
235import StringIO
236
237import MySQLdb
238import MySQLdb.cursors
239from trac.env import Environment
240
241if not hasattr(sys, 'setdefaultencoding'):
242    reload(sys)
243
244sys.setdefaultencoding('latin1')
245
246# simulated Attachment class for trac.add
247class Attachment:
248    def __init__(self, name, data):
249        self.filename = name
250        self.file = StringIO.StringIO(data.tostring())
251 
252# simple field translation mapping.  if string not in
253# mapping, just return string, otherwise return value
254class FieldTranslator(dict):
255    def __getitem__(self, item):
256        if not dict.has_key(self, item):
257            return item
258           
259        return dict.__getitem__(self, item)
260
261statusXlator = FieldTranslator(STATUS_TRANSLATE)
262
263class TracDatabase(object):
264    def __init__(self, path):
265        self.env = Environment(path)
266        self._db = self.env.get_db_cnx()
267        self._db.autocommit = False
268        self.loginNameCache = {}
269        self.fieldNameCache = {}
270   
271    def db(self):
272        return self._db
273   
274    def hasTickets(self):
275        c = self.db().cursor()
276        c.execute('''SELECT count(*) FROM Ticket''')
277        return int(c.fetchall()[0][0]) > 0
278
279    def assertNoTickets(self):
280        if self.hasTickets():
281            raise Exception("Will not modify database with existing tickets!")
282   
283    def setSeverityList(self, s):
284        """Remove all severities, set them to `s`"""
285        self.assertNoTickets()
286       
287        c = self.db().cursor()
288        c.execute("""DELETE FROM enum WHERE type='severity'""")
289        for value, i in s:
290            print "inserting severity ", value, " ", i
291            c.execute("""INSERT INTO enum (type, name, value) VALUES (%s, %s, %s)""",
292                      ("severity", value.encode('utf-8'), i,))
293        self.db().commit()
294   
295    def setPriorityList(self, s):
296        """Remove all priorities, set them to `s`"""
297        self.assertNoTickets()
298       
299        c = self.db().cursor()
300        c.execute("""DELETE FROM enum WHERE type='priority'""")
301        for value, i in s:
302            print "inserting priority ", value, " ", i
303            c.execute("""INSERT INTO enum (type, name, value) VALUES (%s, %s, %s)""",
304                      ("priority", value.encode('utf-8'), i,))
305        self.db().commit()
306
307   
308    def setComponentList(self, l, key):
309        """Remove all components, set them to `l`"""
310        self.assertNoTickets()
311       
312        c = self.db().cursor()
313        c.execute("""DELETE FROM component""")
314        for comp in l:
315            print "inserting component '",comp[key],"', owner",  comp['owner']
316            c.execute("""INSERT INTO component (name, owner) VALUES (%s, %s)""",
317                      (comp[key].encode('utf-8'), comp['owner'].encode('utf-8'),))
318        self.db().commit()
319   
320    def setVersionList(self, v, key):
321        """Remove all versions, set them to `v`"""
322        self.assertNoTickets()
323       
324        c = self.db().cursor()
325        c.execute("""DELETE FROM version""")
326        for vers in v:
327            print "inserting version ", vers[key]
328            c.execute("""INSERT INTO version (name) VALUES (%s)""",
329                      (vers[key].encode('utf-8'),))
330        self.db().commit()
331       
332    def setMilestoneList(self, m, key):
333        """Remove all milestones, set them to `m`"""
334        self.assertNoTickets()
335       
336        c = self.db().cursor()
337        c.execute("""DELETE FROM milestone""")
338        for ms in m:
339            print "inserting milestone ", ms[key]
340            c.execute("""INSERT INTO milestone (name) VALUES (%s)""",
341                      (ms[key].encode('utf-8'),))
342        self.db().commit()
343   
344    def addTicket(self, id, time, changetime, component,
345                  severity, priority, owner, reporter, cc,
346                  version, milestone, status, resolution,
347                  summary, description, keywords):
348        c = self.db().cursor()
349       
350        desc = description.encode('utf-8')
351       
352        if PREFORMAT_COMMENTS:
353          desc = '{{{\n%s\n}}}' % desc
354
355        print "inserting ticket %s -- \"%s\"" % (id, summary[0:40].replace("\n", " "))
356        c.execute("""INSERT INTO ticket (id, time, changetime, component,
357                                         severity, priority, owner, reporter, cc,
358                                         version, milestone, status, resolution,
359                                         summary, description, keywords)
360                                 VALUES (%s, %s, %s, %s,
361                                         %s, %s, %s, %s, %s,
362                                         %s, %s, %s, %s,
363                                         %s, %s, %s)""",
364                  (id, time.strftime('%s'), changetime.strftime('%s'), component.encode('utf-8'),
365                  severity.encode('utf-8'), priority.encode('utf-8'), owner, reporter, cc,
366                  version, milestone.encode('utf-8'), status.lower(), resolution,
367                  summary.encode('utf-8'), desc, keywords,))
368       
369        self.db().commit()
370       
371        c.execute('''SELECT last_insert_rowid()''')
372        return c.fetchall()[0][0]
373        #return self.db().db.sqlite_last_insert_rowid()
374   
375    def addTicketComment(self, ticket, time, author, value):
376        print " * adding comment \"%s...\"" % value[0:40]
377        comment = value.encode('utf-8')
378       
379        if PREFORMAT_COMMENTS:
380          comment = '{{{\n%s\n}}}' % comment
381
382        c = self.db().cursor()
383        c.execute("""INSERT INTO ticket_change (ticket, time, author, field, oldvalue, newvalue)
384                                 VALUES        (%s, %s, %s, %s, %s, %s)""",
385                  (ticket, time.strftime('%s'), author, 'comment', '', comment,))
386        self.db().commit()
387
388    def addTicketChange(self, ticket, time, author, field, oldvalue, newvalue):
389        print " * adding ticket change \"%s\": \"%s\" -> \"%s\" (%s)" % (field, oldvalue[0:20], newvalue[0:20], time)
390        c = self.db().cursor()
391        c.execute("""INSERT INTO ticket_change (ticket, time, author, field, oldvalue, newvalue)
392                                 VALUES        (%s, %s, %s, %s, %s, %s)""",
393                  (ticket, time.strftime('%s'), author, field, oldvalue.encode('utf-8'), newvalue.encode('utf-8'),))
394        self.db().commit()
395        # Now actually change the ticket because the ticket wont update itself!
396        sql = "UPDATE ticket SET %s='%s' WHERE id=%s" % (field, newvalue, ticket)
397        c.execute(sql)
398        self.db().commit()       
399       
400    def addAttachment(self, id, attachment, description, author):
401        print 'inserting attachment for ticket %s -- %s' % (id, description)
402        attachment.filename = attachment.filename.encode('utf-8')
403        self.env.create_attachment(self.db(), 'ticket', str(id), attachment, description.encode('utf-8'),
404            author, 'unknown')
405       
406    def getLoginName(self, cursor, userid):
407        if userid not in self.loginNameCache:
408            cursor.execute("SELECT * FROM mantis_user_table WHERE id = %s" % userid)
409            loginName = cursor.fetchall()
410
411            if loginName:
412                loginName = loginName[0]['username']
413            else:
414                print 'warning: unknown mantis userid %d, recording as anonymous' % userid
415                loginName = 'anonymous'
416
417            self.loginNameCache[userid] = loginName
418
419        return self.loginNameCache[userid]
420
421
422def productFilter(fieldName, products):
423    first = True
424    result = ''
425    for product in products:
426        if not first: 
427            result += " or "
428        first = False
429        result += "%s = '%s'" % (fieldName, product)
430    return result
431
432def convert(_db, _host, _user, _password, _env, _force):
433    activityFields = FieldTranslator()
434
435    # account for older versions of mantis
436    if MANTIS_VERSION == '0.19':
437        print 'Using Mantis v%s schema.' % MANTIS_VERSION
438        activityFields['removed'] = 'oldvalue'
439        activityFields['added'] = 'newvalue'
440
441    # init Mantis environment
442    print "Mantis MySQL('%s':'%s':'%s':'%s'): connecting..." % (_db, _host, _user, _password)
443    mysql_con = MySQLdb.connect(host=_host,