Edgewall Software

Ticket #5855: mantis2py.patch

File mantis2py.patch, 40.2 KB (added by licho@…, 5 years ago)
  • contrib/mantis2trac.py

     
     1#!/usr/bin/env python 
     2 
     3""" 
     4Import Mantis bugs into a Trac database. 
     5 
     6Requires:  Trac 0.9.X or newer from http://trac.edgewall.com/ 
     7           Python 2.4 from http://www.python.org/ 
     8           MySQL >= 3.23 from http://www.mysql.org/ 
     9 
     10Version 1.4 
     11Author: John Lichovnik (licho@ufo.cz) 
     12Date: 10.9.2007 
     13 
     14Version 1.3 
     15Author: Anton Stroganov (stroganov.a@gmail.com) 
     16Date: December 19, 2006 
     17 
     18Based on version 1.1 from: 
     19Author: Joao Prado Maia (jpm@pessoal.org) 
     20 
     21Based on version 1.0 from: 
     22Paul Baranowski (paul@paulbaranowski.org) 
     23 
     24Based on bugzilla2trac.py by these guys (thank you!): 
     25Dmitry Yusupov <dmitry_yus@yahoo.com> - bugzilla2trac.py 
     26Mark Rowe <mrowe@bluewire.net.nz> - original TracDatabase class 
     27Bill Soudan <bill@soudan.net> - Many enhancements  
     28 
     29Example use: 
     30  python mantis2trac.py --db mantis --tracenv /usr/local/trac-projects/myproj/ \ 
     31    --host localhost --user root --clean --products foo,bar 
     32 
     33Changes in version 1.4: 
     34  - fixed strftime for Python 2.4 
     35  - fixed Mantis text_id in ticket and comment queries (original version was sometimes adding mismatched descriptions and comments) 
     36  - added IGNORE_VERSION switch 
     37 
     38Changes since version 1.2: 
     39  - better join in the attachment author finding query 
     40  - changed default encoding to be utf8 
     41  - added working status->keyword migration for statuses that don't have exact Trac equivalents 
     42 
     43Changes since version 1.1: 
     44  - Made it work against Trac running on MySQL (specifically, changes to the  
     45    LAST_INSERT_ID() call on line 382 (in the addTicket function)) 
     46  - Couple of bugfixes 
     47  - Works fine against 10.2 
     48  - Modified to allow specifying product list on command line 
     49  - Modified to migrate database-stored mantis attachments correctly. 
     50      Nota Bene!!! The script requires write access to the attachments  
     51      directory of the trac env. So, suggested sequence of actions:  
     52        - chmod -R 777 /usr/local/trac-projects/myproj/attachments/ 
     53        - run the script 
     54        - chown -R apache /usr/local/trac-projects/myproj/attachments/ 
     55        - chgrp -R webuser /usr/local/trac-projects/myproj/attachments/ 
     56        - chmod -R 755 /usr/local/trac-projects/myproj/attachments/ 
     57 
     58Changes since version 1.0: 
     59  - Made it to work against Trac 0.9.3 (tweaks to make the Environment class work) 
     60  - Re-did all prepared statements-like queries to avoid a DB error 
     61  - Fixed a reference to the wrong variable name when adding a comment 
     62 
     63Notes: 
     64  - Private bugs will become public 
     65  - Some ticket changes will not be preserved since they have no  
     66    equivalents in Trac. 
     67  - I consider milestones and versions to be the same thing (actually, 
     68    I dont really care about the version, because for our project, bugs are  
     69    only in the 'previous version'). 
     70  - Importing attachments is not implemented (couldnt get it to work,  
     71    and we didnt have enough attachments to justify spending time on this) 
     72    "Clean" will not delete your existing attachments.  There is code in here 
     73    to support adding attachments, but you will have to play with it to  
     74    make it work.  If you search for the word "attachment" you will find 
     75    all the code related to this. 
     76  - Ticket descriptions & comments will be re-wrapped to 70 characters. 
     77    This may mess up your formatting for your bugs.  If you dont want to do 
     78    this, search for textwrap.fill() and fix it. 
     79  - You will probably want to change "report.css" in trac to handle one more  
     80    level of priorities (default trac has 6 levels of priorities, while Mantis 
     81    has 7).  When you look at your reports, the color schemes will look wrong. 
     82     
     83    The lines that control the priority color scheme look like this: 
     84    #tktlist tr.color1-odd  { background: #fdc; border-color: #e88; color: #a22 } 
     85    #tktlist tr.color1-even { background: #fed; border-color: #e99; color: #a22 } 
     86     
     87    I added a new level 2 ("urgent") with an orange color,  
     88    and incremented all the rest of the levels: 
     89    #tktlist tr.color2-odd  { background: #FFE08F; border-color: #e88; color: #a22 } 
     90    #tktlist tr.color2-even { background: #FFE59F; border-color: #e99; color: #a22 } 
     91     
     92""" 
     93from urllib import quote 
     94import datetime 
     95import time 
     96 
     97### 
     98### Conversion Settings -- edit these before running if desired 
     99### 
     100 
     101# Mantis version.   
     102# 
     103# Currently, the following mantis versions are known to work: 
     104#   0.19.X 
     105# 
     106# If you run this script on a version not listed here and it is successful, 
     107# please report it to the Trac mailing list so we can update the list. 
     108MANTIS_VERSION = '0.19' 
     109 
     110# MySQL connection parameters for the Mantis database.  These can also  
     111# be specified on the command line. 
     112MANTIS_DB = 'mantis' 
     113MANTIS_HOST = 'localhost' 
     114MANTIS_USER = 'root' 
     115MANTIS_PASSWORD = '' 
     116 
     117# Path to the Trac environment. 
     118TRAC_ENV = '' 
     119 
     120# If true, all existing Trac tickets will be removed  
     121# prior to import. 
     122TRAC_CLEAN = True 
     123 
     124# Enclose imported ticket description and comments in a {{{ }}}  
     125# preformat block?  This formats the text in a fixed-point font. 
     126PREFORMAT_COMMENTS = False 
     127 
     128# Products are now specified on command line. 
     129# By default, all bugs are imported from Mantis.  If you add a list 
     130# of products here, only bugs from those products will be imported. 
     131# Warning: I have not tested this script where this field is blank! 
     132# default products to ignore: 
     133PRODUCTS = [ ] 
     134 
     135# Trac doesn't have the concept of a product.  Instead, this script can 
     136# assign keywords in the ticket entry to represent products. 
     137# 
     138# ex. PRODUCT_KEYWORDS = { 'product1' : 'PRODUCT1_KEYWORD' } 
     139PRODUCT_KEYWORDS = {} 
     140 
     141# Bug comments that should not be imported.  Each entry in list should 
     142# be a regular expression. 
     143IGNORE_COMMENTS = [ 
     144#   '^Created an attachment \(id=' 
     145] 
     146 
     147# Ticket changes in Trac have the restriction where the 
     148# bug ID, field, and time must be unique for all entries in the ticket  
     149# changes table. 
     150# Mantis, for unknown reasons, has fields that can change two states  
     151# in under a second (e.g. "milestone":""->"1.0", "milestone":"1.0"->"2.0"). 
     152# Setting this to true will attempt to fix these cases by adjusting the  
     153# time for the 2nd change to be one second more than the original time. 
     154# I dont know why you'd want to turn this off, but I give you the option  
     155# anyhow. :) 
     156TIME_ADJUSTMENT_HACK = True 
     157 
     158# If set to true, version numbers wont be assigned to tickets (just milestones) 
     159IGNORE_VERSION = True 
     160 
     161########################################################################### 
     162### You probably don't need to change any configuration past this line. ### 
     163########################################################################### 
     164 
     165# Mantis status to Trac status translation map. 
     166# 
     167# NOTE: bug activity is translated as well, which may cause bug 
     168# activity to be deleted (e.g. resolved -> closed in Mantis 
     169# would translate into closed -> closed in Trac, so we just ignore the 
     170# change). 
     171# 
     172# Possible Trac 'status' values: 'new', 'assigned', 'reopened', 'closed' 
     173STATUS_TRANSLATE = { 
     174  10 : 'new',      # 10 == 'new' in mantis 
     175  20 : 'assigned', # 20 == 'feedback' 
     176  30 : 'new',      # 30 == 'acknowledged'  
     177  40 : 'new',      # 40 == 'confirmed' 
     178  50 : 'assigned', # 50 == 'assigned'  
     179  60 : 'assigned', # 60 == 'QA' 
     180  80 : 'closed',   # 80 == 'resolved'  
     181  90 : 'closed'    # 90 == 'closed' 
     182} 
     183 
     184# Unused: 
     185# Translate Mantis statuses into Trac keywords.  This provides a way  
     186# to retain the Mantis statuses in Trac.  e.g. when a bug is marked  
     187# 'verified' in Mantis it will be assigned a VERIFIED keyword. 
     188# STATUS_KEYWORDS = { 
     189 
     190STATUS_KEYWORDS = { 
     191    20 : 'FEEDBACK', 
     192    30 : 'ACKNOWLEDGED', 
     193    40 : 'CONFIRMED', 
     194    60 : 'QA', 
     195    80 : 'RESOLVED'  
     196} 
     197 
     198# Possible Trac resolutions are 'fixed', 'invalid', 'wontfix', 'duplicate', 'worksforme' 
     199RESOLUTION_TRANSLATE = { 
     200    10 : '',          # 10 == 'open' in mantis 
     201    20 : 'fixed',     # 20 == 'fixed' 
     202    30 : '',          # 30 == 'reopened' (TODO: 'reopened' needs to be mapped to a status event) 
     203    40 : 'invalid',   # 40 == 'unable to duplicate' 
     204    50 : 'wontfix',   # 50 == 'not fixable' 
     205    60 : 'duplicate', # 60 == 'duplicate' 
     206    70 : 'invalid',   # 70 == 'not an issue' 
     207    80 : '',          # 80 == 'suspended' 
     208    90 : 'wontfix',   # 90 == 'wont fix' 
     209} 
     210 
     211# Mantis severities (which will also become equivalent Trac severities) 
     212##SEVERITY_LIST = (('block', '80'),  
     213##                 ('crash', '70'),  
     214##                 ('major', '60'),  
     215##                 ('minor', '50'), 
     216##                 ('tweak', '40'),  
     217##                 ('text', '30'),  
     218##                 ('trivial', '20'),  
     219##                 ('feature', '10')) 
     220SEVERITY_LIST = (('block', '1'),  
     221                 ('crash', '2'),  
     222                 ('major', '3'),  
     223                 ('minor', '4'), 
     224                 ('tweak', '5'),  
     225                 ('text', '6'),  
     226                 ('trivial', '7'),  
     227                 ('feature', '8')) 
     228 
     229# Translate severity numbers into their text equivalents 
     230SEVERITY_TRANSLATE = { 
     231    80 : 'block', 
     232    70 : 'crash', 
     233    60 : 'major', 
     234    50 : 'minor', 
     235    40 : 'tweak', 
     236    30 : 'text', 
     237    20 : 'trivial', 
     238    10 : 'feature' 
     239} 
     240 
     241# Mantis priorities (which will also become Trac priorities) 
     242##PRIORITY_LIST = (('immediate', '60'),  
     243##                 ('urgent', '50'),  
     244##                 ('high', '40'),  
     245##                 ('normal', '30'),  
     246##                 ('low', '20'),  
     247##                 ('none', '10')) 
     248PRIORITY_LIST = (('immediate', '1'),  
     249                 ('urgent', '2'),  
     250                 ('high', '3'),  
     251                 ('normal', '4'),  
     252                 ('low', '5'),  
     253                 ('none', '6')) 
     254 
     255# Translate priority numbers into their text equivalent 
     256PRIORITY_TRANSLATE = { 
     257    60 : 'immediate',  
     258    50 : 'urgent',  
     259    40 : 'high', 
     260    30 : 'normal',  
     261    20 : 'low',  
     262    10 : 'none' 
     263} 
     264 
     265 
     266# Some fields in Mantis do not have equivalents in Trac.  Changes in 
     267# fields listed here will not be imported into the ticket change history, 
     268# otherwise you'd see changes for fields that don't exist in Trac. 
     269IGNORED_ACTIVITY_FIELDS = ['', 'project_id', 'reproducibility', 'view_state', 'os', 'os_build', 'duplicate_id'] 
     270 
     271### 
     272### Script begins here 
     273### 
     274 
     275import os 
     276import re 
     277import sys 
     278import string 
     279import StringIO 
     280 
     281import MySQLdb 
     282import MySQLdb.cursors 
     283from trac.env import Environment 
     284 
     285if not hasattr(sys, 'setdefaultencoding'): 
     286    reload(sys) 
     287 
     288#sys.setdefaultencoding('utf-8') 
     289 
     290# simulated Attachment class for trac.add 
     291# unused in 1.2 
     292class Attachment: 
     293    def __init__(self, name, data): 
     294        self.filename = name 
     295        self.file = StringIO.StringIO(data.tostring()) 
     296   
     297# simple field translation mapping.  if string not in 
     298# mapping, just return string, otherwise return value 
     299class FieldTranslator(dict): 
     300    def __getitem__(self, item): 
     301        if not dict.has_key(self, item): 
     302            return item 
     303             
     304        return dict.__getitem__(self, item) 
     305 
     306statusXlator = FieldTranslator(STATUS_TRANSLATE) 
     307 
     308class TracDatabase(object): 
     309    def __init__(self, path): 
     310        self.env = Environment(path) 
     311        self._db = self.env.get_db_cnx() 
     312        self._db.autocommit = False 
     313        self.loginNameCache = {} 
     314        self.fieldNameCache = {} 
     315     
     316    def db(self): 
     317        return self._db 
     318     
     319    def hasTickets(self): 
     320        c = self.db().cursor() 
     321        c.execute('''SELECT count(*) FROM ticket''') 
     322        return int(c.fetchall()[0][0]) > 0 
     323 
     324    def assertNoTickets(self): 
     325        if self.hasTickets(): 
     326            raise Exception("Will not modify database with existing tickets!") 
     327     
     328    def setSeverityList(self, s): 
     329        """Remove all severities, set them to `s`""" 
     330        self.assertNoTickets() 
     331         
     332        c = self.db().cursor() 
     333        c.execute("""DELETE FROM enum WHERE type='severity'""") 
     334        for value, i in s: 
     335            print "inserting severity ", value, " ", i 
     336            c.execute("""INSERT INTO enum (type, name, value) VALUES (%s, %s, %s)""", 
     337                      ("severity", value, i,)) 
     338        self.db().commit() 
     339     
     340    def setPriorityList(self, s): 
     341        """Remove all priorities, set them to `s`""" 
     342        self.assertNoTickets() 
     343         
     344        c = self.db().cursor() 
     345        c.execute("""DELETE FROM enum WHERE type='priority'""") 
     346        for value, i in s: 
     347            print "inserting priority ", value, " ", i 
     348            c.execute("""INSERT INTO enum (type, name, value) VALUES (%s, %s, %s)""", 
     349                      ("priority", value, i,)) 
     350        self.db().commit() 
     351 
     352     
     353    def setComponentList(self, l, key): 
     354        """Remove all components, set them to `l`""" 
     355        self.assertNoTickets() 
     356         
     357        c = self.db().cursor() 
     358        c.execute("""DELETE FROM component""") 
     359        for comp in l: 
     360            print "inserting component '",comp[key],"', owner",  comp['owner'] 
     361            c.execute("""INSERT INTO component (name, owner) VALUES (%s, %s)""", 
     362                      (comp[key], comp['owner'],)) 
     363        self.db().commit() 
     364     
     365    def setVersionList(self, v, key): 
     366        """Remove all versions, set them to `v`""" 
     367        self.assertNoTickets() 
     368         
     369        c = self.db().cursor() 
     370        c.execute("""DELETE FROM version""") 
     371        for vers in v: 
     372            print "inserting version ", vers[key] 
     373            c.execute("""INSERT INTO version (name) VALUES (%s)""", 
     374                      (vers[key],)) 
     375        self.db().commit() 
     376         
     377    def setMilestoneList(self, m, key): 
     378        """Remove all milestones, set them to `m`""" 
     379        self.assertNoTickets() 
     380         
     381        c = self.db().cursor() 
     382        c.execute("""DELETE FROM milestone""") 
     383        for ms in m: 
     384            print "inserting milestone ", ms[key] 
     385            c.execute("""INSERT INTO milestone (name) VALUES (%s)""", 
     386                      (ms[key],)) 
     387        self.db().commit() 
     388     
     389    def addTicket(self, id, time, changetime, component, 
     390                  severity, priority, owner, reporter, cc, 
     391                  version, milestone, status, resolution, 
     392                  summary, description, keywords): 
     393        c = self.db().cursor() 
     394        if IGNORE_VERSION: 
     395          version='' 
     396         
     397        desc = description 
     398         
     399        if PREFORMAT_COMMENTS: 
     400          desc = '{{{\n%s\n}}}' % desc 
     401 
     402        print "inserting ticket %s -- \"%s\"" % (id, summary[0:40].replace("\n", " ")) 
     403        c.execute("""INSERT INTO ticket (id, time, changetime, component, 
     404                                         severity, priority, owner, reporter, cc, 
     405                                         version, milestone, status, resolution, 
     406                                         summary, description, keywords) 
     407                                 VALUES (%s, %s, %s, %s, 
     408                                         %s, %s, %s, %s, %s, 
     409                                         %s, %s, %s, %s, 
     410                                         %s, %s, %s)""", 
     411                  (id, self.convertTime(time), self.convertTime(changetime), component, 
     412                  severity, priority, owner, reporter, cc, 
     413                  version, milestone, status.lower(), resolution, 
     414                  summary, desc, keywords)) 
     415         
     416        self.db().commit() 
     417         
     418        ## TODO: add database-specific methods to get the last inserted ticket's id... 
     419        ## PostgreSQL: 
     420        # c.execute('''SELECT currval("ticket_id_seq")''') 
     421        ## SQLite: 
     422        # c.execute('''SELECT last_insert_rowid()''') 
     423        ## MySQL: 
     424        # c.execute('''SELECT LAST_INSERT_ID()''') 
     425        # Oh, Trac db abstraction layer already has a function for this... 
     426        return self.db().get_last_id(c,'ticket') 
     427 
     428    def convertTime(self,time2): 
     429        return time.mktime(time2.timetuple())+1e-6*time2.microsecond 
     430     
     431    def addTicketComment(self, ticket, time, author, value): 
     432        print " * adding comment \"%s...\"" % value[0:40] 
     433        comment = value 
     434         
     435        if PREFORMAT_COMMENTS: 
     436          comment = '{{{\n%s\n}}}' % comment 
     437 
     438        c = self.db().cursor() 
     439        c.execute("""INSERT INTO ticket_change (ticket, time, author, field, oldvalue, newvalue) 
     440                                 VALUES        (%s, %s, %s, %s, %s, %s)""", 
     441                  (ticket, self.convertTime(time), author, 'comment', '', comment)) 
     442        self.db().commit() 
     443 
     444    def addTicketChange(self, ticket, time, author, field, oldvalue, newvalue): 
     445        if (field[0:4]=='doba'):  
     446          return 
     447 
     448        print " * adding ticket change \"%s\": \"%s\" -> \"%s\" (%s)" % (field, oldvalue[0:20], newvalue[0:20], time) 
     449        c = self.db().cursor() 
     450        c.execute("""INSERT INTO ticket_change (ticket, time, author, field, oldvalue, newvalue) 
     451                                 VALUES        (%s, %s, %s, %s, %s, %s)""", 
     452                  (ticket, self.convertTime(time), author, field, oldvalue, newvalue)) 
     453        self.db().commit() 
     454        # Now actually change the ticket because the ticket wont update itself! 
     455        sql = "UPDATE ticket SET %s='%s' WHERE id=%s" % (field, newvalue, ticket) 
     456        c.execute(sql) 
     457        self.db().commit()         
     458         
     459    # unused in 1.2 
     460    def addAttachment(self, id, attachment, description, author): 
     461        print 'inserting attachment for ticket %s -- %s' % (id, description) 
     462        attachment.filename = attachment.filename 
     463        self.env.create_attachment(self.db(), 'ticket', str(id), attachment, description, 
     464            author, 'unknown') 
     465         
     466    def getLoginName(self, cursor, userid): 
     467        if userid not in self.loginNameCache: 
     468            cursor.execute("SELECT username,email,realname,last_visit FROM mantis_user_table WHERE id = %i" % int(userid)) 
     469            result = cursor.fetchall() 
     470 
     471            if result: 
     472                loginName = result[0]['username'] 
     473                print 'Adding user %s to sessions table' % loginName 
     474                c = self.db().cursor() 
     475 
     476                # check if user is already in the sessions table 
     477                c.execute("SELECT sid FROM session WHERE sid = '%s'" % result[0]['username']) 
     478                r = c.fetchall() 
     479                 
     480                # if there was no user sid in the database already 
     481                if not r: 
     482                    # pre-populate the session table and the realname/email table with user data 
     483                    try: 
     484                        c.execute( 
     485                        """INSERT INTO session  
     486                            (sid, authenticated, last_visit)  
     487                        VALUES (%s, %s, %s)""",(result[0]['username'], '1', self.convertTime(result[0]['last_visit']))) 
     488                    except: 
     489                        print 'failed executing sql: ' 
     490                        print """INSERT INTO session  
     491                            (sid, authenticated, last_visit)  
     492                        VALUES """, (result[0]['username'], '1', self.convertTime(result[0]['last_visit'])) 
     493                        print 'could not insert %s into sessions table: sql error %s ' % (loginName, self.db().error()) 
     494                    self.db().commit() 
     495                 
     496                    # insert the user's real name into session attribute table 
     497                    c.execute( 
     498                        """INSERT INTO session_attribute  
     499                            (sid, authenticated, name, value) 
     500                        VALUES 
     501                            (%s, %s, %s, %s)""", (result[0]['username'], '1', 'name', result[0]['realname'])) 
     502                    self.db().commit() 
     503 
     504                    # insert the user's email into session attribute table 
     505                    c.execute( 
     506                        """INSERT INTO session_attribute  
     507                            (sid, authenticated, name, value) 
     508                        VALUES 
     509                            (%s, %s, %s, %s)""", (result[0]['username'], '1', 'email', result[0]['email'])) 
     510                    self.db().commit() 
     511            else: 
     512                print 'warning: unknown mantis userid %d, recording as anonymous' % userid 
     513                loginName = '' 
     514 
     515            self.loginNameCache[userid] = loginName 
     516 
     517        return self.loginNameCache[userid] 
     518 
     519    def get_attachments_dir(self,bugid=0): 
     520        if bugid > 0: 
     521            return self.env.path + 'attachments/ticket/%i/' % bugid         
     522        else: 
     523            return self.env.path + 'attachments/ticket/' 
     524 
     525    def _mkdir(newdir): 
     526        """works the way a good mkdir should :) 
     527            - already exists, silently complete 
     528            - regular file in the way, raise an exception 
     529            - parent directory(ies) does not exist, make them as well 
     530        """ 
     531        if os.path.isdir(newdir): 
     532            pass 
     533        elif os.path.isfile(newdir): 
     534            raise OSError("a file with the same name as the desired " \ 
     535                          "dir, '%s', already exists." % newdir) 
     536        else: 
     537            head, tail = os.path.split(newdir) 
     538            if head and not os.path.isdir(head): 
     539                _mkdir(head) 
     540            #print "_mkdir %s" % repr(newdir) 
     541            if tail: 
     542                os.mkdir(newdir) 
     543 
     544def productFilter(fieldName, products): 
     545    first = True 
     546    result = '' 
     547    for product in products: 
     548        if not first:  
     549            result += " or " 
     550        first = False 
     551        result += "%s = '%s'" % (fieldName, product) 
     552    return result 
     553 
     554def convert(_db, _host, _user, _password, _env, _force): 
     555    activityFields = FieldTranslator() 
     556 
     557    # account for older versions of mantis 
     558    if MANTIS_VERSION == '0.19': 
     559        print 'Using Mantis v%s schema.' % MANTIS_VERSION 
     560        activityFields['removed'] = 'oldvalue' 
     561        activityFields['added'] = 'newvalue' 
     562 
     563    # init Mantis environment 
     564    print "Mantis MySQL('%s':'%s':'%s':'%s'): connecting..." % (_db, _host, _user, _password) 
     565    mysql_con = MySQLdb.connect(host=_host,  
     566                user=_user, passwd=_password, db=_db, compress=1,  
     567                cursorclass=MySQLdb.cursors.DictCursor) 
     568    mysql_cur = mysql_con.cursor() 
     569 
     570    # init Trac environment 
     571    print "Trac database('%s'): connecting..." % (_env) 
     572    trac = TracDatabase(_env) 
     573 
     574    # force mode... 
     575    if _force == 1: 
     576        print "cleaning all tickets..." 
     577        c = trac.db().cursor() 
     578        c.execute("""DELETE FROM ticket_change""") 
     579        trac.db().commit() 
     580        c.execute("""DELETE FROM ticket""") 
     581        trac.db().commit() 
     582        c.execute("""DELETE FROM attachment""") 
     583        os.system('rm -rf %s' % trac.get_attachments_dir()) 
     584        os.mkdir(trac.get_attachments_dir()) 
     585        trac.db().commit() 
     586 
     587    print 
     588    print '0. Finding project IDs...' 
     589    sql =  "SELECT id, name FROM mantis_project_table" 
     590    if PRODUCTS: 
     591        sql += " WHERE %s" % productFilter('name', PRODUCTS) 
     592    mysql_cur.execute(sql) 
     593    project_list = mysql_cur.fetchall() 
     594    project_dict = dict() 
     595    for project_id in project_list: 
     596        print "Mantis project name '%s' has project ID %s" % (project_id['name'], project_id['id']) 
     597        project_dict[project_id['id']] = project_id['id'] 
     598         
     599    print 
     600    print "1. import severities..." 
     601    trac.setSeverityList(SEVERITY_LIST) 
     602 
     603    print 
     604    print "2. import components..." 
     605    sql = "SELECT category, user_id as owner FROM mantis_project_category_table" 
     606    if PRODUCTS: 
     607       sql += " WHERE %s" % productFilter('project_id', project_dict) 
     608    print "sql: %s" % sql 
     609    mysql_cur.execute(sql) 
     610    components = mysql_cur.fetchall() 
     611    for component in components: 
     612        component['owner'] = trac.getLoginName(mysql_cur, component['owner']) 
     613    trac.setComponentList(components, 'category') 
     614 
     615    print 
     616    print "3. import priorities..." 
     617    trac.setPriorityList(PRIORITY_LIST) 
     618 
     619    print 
     620    print "4. import versions..." 
     621    sql = "SELECT DISTINCTROW version FROM mantis_project_version_table" 
     622    if PRODUCTS: 
     623       sql += " WHERE %s" % productFilter('project_id', project_dict) 
     624    mysql_cur.execute(sql) 
     625    versions = mysql_cur.fetchall() 
     626    trac.setVersionList(versions, 'version') 
     627 
     628    print 
     629    print "5. import milestones..." 
     630    sql = "SELECT version FROM mantis_project_version_table" 
     631    if PRODUCTS: 
     632       sql += " WHERE %s" % productFilter('project_id', project_dict) 
     633    mysql_cur.execute(sql) 
     634    milestones = mysql_cur.fetchall() 
     635    trac.setMilestoneList(milestones, 'version') 
     636 
     637    print 
     638    print '6. retrieving bugs...' 
     639    sql = "SELECT * FROM mantis_bug_table " 
     640    if PRODUCTS: 
     641       sql += " WHERE %s" % productFilter('project_id', project_dict) 
     642    sql += " ORDER BY id" 
     643    mysql_cur.execute(sql) 
     644    bugs = mysql_cur.fetchall() 
     645     
     646    print 
     647    print "7. import bugs and bug activity..." 
     648    totalComments = 0 
     649    totalTicketChanges = 0 
     650    totalAttachments = 0 
     651    errors = [] 
     652    timeAdjustmentHacks = [] 
     653    for bug in bugs: 
     654        bugid = bug['id'] 
     655         
     656        ticket = {} 
     657        keywords = [] 
     658        ticket['id'] = bugid 
     659        ticket['time'] = bug['date_submitted'] 
     660        ticket['changetime'] = bug['last_updated'] 
     661        ticket['component'] = bug['category'] 
     662        ticket['severity'] = SEVERITY_TRANSLATE[bug['severity']] 
     663        ticket['priority'] = PRIORITY_TRANSLATE[bug['priority']] 
     664        ticket['owner'] = trac.getLoginName(mysql_cur, bug['handler_id']) 
     665        ticket['reporter'] = trac.getLoginName(mysql_cur, bug['reporter_id']) 
     666        ticket['version'] = bug['version'] 
     667        if IGNORE_VERSION: 
     668          ticket['version'] = '' 
     669        ticket['milestone'] = bug['version'] 
     670        ticket['summary'] = bug['summary'] 
     671        ticket['status'] = STATUS_TRANSLATE[bug['status']] 
     672        ticket['cc'] = '' 
     673        ticket['keywords'] = '' 
     674 
     675        # Special case for 'reopened' resolution in mantis -  
     676        # it maps to a status type in Trac. 
     677        if (bug['resolution'] == 30): 
     678            ticket['status'] = 'reopened' 
     679        ticket['resolution'] = RESOLUTION_TRANSLATE[bug['resolution']] 
     680         
     681        # Compose the description from the three text fields in Mantis: 
     682        # 'description', 'steps_to_reproduce', 'additional_information' 
     683        mysql_cur.execute("SELECT * FROM mantis_bug_text_table WHERE id = %s" % bug['bug_text_id'])  
     684        longdescs = list(mysql_cur.fetchall()) 
     685 
     686        # check for empty 'longdescs[0]' field... 
     687        if len(longdescs) == 0: 
     688            ticket['description'] = '' 
     689        else: 
     690            tmpDescr = longdescs[0]['description'] 
     691            if (longdescs[0]['steps_to_reproduce'].strip() != ''): 
     692               tmpDescr = ('%s\n\nSTEPS TO REPRODUCE:\n%s') % (tmpDescr, longdescs[0]['steps_to_reproduce']) 
     693            if (longdescs[0]['additional_information'].strip() != ''): 
     694               tmpDescr = ('%s\n\nADDITIONAL INFORMATION:\n%s') % (tmpDescr, longdescs[0]['additional_information']) 
     695            ticket['description'] = tmpDescr 
     696            del longdescs[0] 
     697 
     698        # Add the ticket to the Trac database 
     699        trac.addTicket(**ticket) 
     700         
     701        # 
     702        # Add ticket comments 
     703        # 
     704        mysql_cur.execute("SELECT * FROM mantis_bugnote_table, mantis_bugnote_text_table WHERE bug_id = %s AND mantis_bugnote_table.bugnote_text_id = mantis_bugnote_text_table.id ORDER BY date_submitted" % bugid) 
     705        bug_notes = mysql_cur.fetchall() 
     706        totalComments += len(bug_notes) 
     707        for note in bug_notes: 
     708            trac.addTicketComment(bugid, note['date_submitted'], trac.getLoginName(mysql_cur, note['reporter_id']), note['note']) 
     709 
     710        # 
     711        # Convert ticket changes 
     712        # 
     713        mysql_cur.execute("SELECT * FROM mantis_bug_history_table WHERE bug_id = %s ORDER BY date_modified" % bugid) 
     714        bugs_activity = mysql_cur.fetchall() 
     715        resolution = '' 
     716        ticketChanges = [] 
     717        keywords = [] 
     718        for activity in bugs_activity: 
     719            field_name = activity['field_name'].lower() 
     720            # Convert Mantis field names... 
     721            # The following fields are the same in Mantis and Trac: 
     722            #  - 'status' 
     723            #  - 'priority' 
     724            #  - 'summary' 
     725            #  - 'resolution' 
     726            #  - 'severity' 
     727            #  - 'version' 
     728            # 
     729            # Ignore the following changes: 
     730            #  - project_id 
     731            #  - reproducibility 
     732            #  - view_state 
     733            #  - os 
     734            #  - os_build 
     735            #  - duplicate_id 
     736            # 
     737            # Convert Mantis -> Trac: 
     738            #  - 'handler_id' -> 'owner' 
     739            #  - 'fixed_in_version' -> 'milestone' 
     740            #  - 'category' -> 'component' 
     741            #  - 'version' -> 'milestone' 
     742             
     743            ticketChange = {} 
     744            ticketChange['ticket'] = bugid 
     745            ticketChange['oldvalue'] = activity['old_value'] 
     746            ticketChange['newvalue'] = activity['new_value'] 
     747            ticketChange['time'] = activity['date_modified'] 
     748            ticketChange['author'] = trac.getLoginName(mysql_cur, activity['user_id']) 
     749            ticketChange['field'] = field_name 
     750 
     751            add_keywords = [] 
     752            remove_keywords = [] 
     753             
     754            if field_name == 'handler_id': 
     755                ticketChange['field'] = 'owner' 
     756                ticketChange['oldvalue'] = trac.getLoginName(mysql_cur, int(activity['old_value'])) 
     757                ticketChange['newvalue'] = trac.getLoginName(mysql_cur, int(activity['new_value'])) 
     758            elif field_name == 'fixed_in_version': 
     759                ticketChange['field'] = 'milestone' 
     760            elif field_name == 'category': 
     761                ticketChange['field'] = 'component' 
     762            elif field_name == 'version': 
     763                ticketChange['field'] = 'milestone' 
     764            elif field_name == 'status': 
     765                ticketChange['oldvalue'] = STATUS_TRANSLATE[int(activity['old_value'])] 
     766                ticketChange['newvalue'] = STATUS_TRANSLATE[int(activity['new_value'])] 
     767                if int(activity['old_value']) in STATUS_KEYWORDS: 
     768                    remove_keywords.append(STATUS_KEYWORDS[int(activity['old_value'])]) 
     769                if int(activity['new_value']) in STATUS_KEYWORDS: 
     770                    add_keywords.append(STATUS_KEYWORDS[int(activity['new_value'])]) 
     771                 
     772            elif field_name == 'priority': 
     773                ticketChange['oldvalue'] = PRIORITY_TRANSLATE[int(activity['old_value'])] 
     774                ticketChange['newvalue'] = PRIORITY_TRANSLATE[int(activity['new_value'])] 
     775            elif field_name == 'resolution': 
     776                ticketChange['oldvalue'] = RESOLUTION_TRANSLATE[int(activity['old_value'])] 
     777                ticketChange['newvalue'] = RESOLUTION_TRANSLATE[int(activity['new_value'])] 
     778            elif field_name == 'severity': 
     779                ticketChange['oldvalue'] = SEVERITY_TRANSLATE[int(activity['old_value'])] 
     780                ticketChange['newvalue'] = SEVERITY_TRANSLATE[int(activity['new_value'])]             
     781 
     782            if add_keywords or remove_keywords: 
     783                # ensure removed ones are in old 
     784                old_keywords = keywords + [kw for kw in remove_keywords if kw not in keywords] 
     785                # remove from new 
     786                keywords = [kw for kw in keywords if kw not in remove_keywords] 
     787                # add to new 
     788                keywords += [kw for kw in add_keywords if kw not in keywords] 
     789                if old_keywords != keywords: 
     790                    ticketChangeKw = ticketChange.copy() 
     791                    ticketChangeKw['field'] = "keywords" 
     792                    ticketChangeKw['oldvalue'] = ' '.join(old_keywords) 
     793                    ticketChangeKw['newvalue'] = ' '.join(keywords) 
     794                    ticketChanges.append(ticketChangeKw) 
     795                                 
     796            if field_name in IGNORED_ACTIVITY_FIELDS: 
     797                continue 
     798 
     799            # skip changes that have no effect (think translation!) 
     800            if ticketChange['oldvalue'] == ticketChange['newvalue']: 
     801                continue 
     802                 
     803            ticketChanges.append (ticketChange) 
     804 
     805        totalTicketChanges += len(ticketChanges) 
     806        for ticketChange in ticketChanges: 
     807            try: 
     808                trac.addTicketChange (**ticketChange) 
     809            except: 
     810                if TIME_ADJUSTMENT_HACK: 
     811                    addTime = datetime.timedelta(seconds=1) 
     812                    originalTime = ticketChange['time'] 
     813                    ticketChange['time'] += addTime 
     814                    try: 
     815                        trac.addTicketChange(**ticketChange) 
     816                        noticeStr = " ~ Successfully adjusted time for ticket(#%s) change \"%s\": \"%s\" -> \"%s\" (%s)" % (bugid, ticketChange['field'], ticketChange['oldvalue'], ticketChange['newvalue'], ticketChange['time']) 
     817                        noticeStr += "\n   Original time: %s" % originalTime 
     818                        timeAdjustmentHacks.append(noticeStr) 
     819                    except: 
     820                        errorStr =  " * ERROR: unable to add ticket(#%s) change \"%s\": \"%s\" -> \"%s\" (%s)" % (bugid, ticketChange['field'], ticketChange['oldvalue'], ticketChange['newvalue'], ticketChange['time']) 
     821                        errorStr += "\n          The bug id, field name, and time must be unique" 
     822                        errors.append(errorStr) 
     823                        print errorStr 
     824                else: 
     825                    errorStr =  " * ERROR: unable to add ticket(#%s) change \"%s\": \"%s\" -> \"%s\" (%s)" % (bugid, ticketChange['field'], ticketChange['oldvalue'], ticketChange['newvalue'], ticketChange['time']) 
     826                    errorStr += "\n          The bug id, field name, and time must be unique" 
     827                    errors.append(errorStr) 
     828                    print errorStr 
     829                 
     830 
     831        # 
     832        # Add ticket file attachments 
     833        # 
     834        attachment_sql = "SELECT b.id,b.bug_id,b.title,b.description,b.filename,b.filesize,b.file_type,UNIX_TIMESTAMP(b.date_added) AS date_added, b.content, h.user_id FROM mantis_bug_file_table AS b LEFT JOIN mantis_bug_history_table AS h ON (h.type = 9 AND h.old_value = b.filename AND h.bug_id = b.bug_id) WHERE b.bug_id = %s" % bugid 
     835        # print attachment_sql 
     836        mysql_cur.execute(attachment_sql) 
     837        attachments = mysql_cur.fetchall() 
     838        for attachment in attachments: 
     839            author = trac.getLoginName(mysql_cur, attachment['user_id']) 
     840 
     841            # Old attachment stuff that never worked... 
     842            # attachmentFile = open(attachment['diskfile'], 'r') 
     843            # attachmentData = attachmentFile.read() 
     844            # tracAttachment = Attachment(attachment['filename'], attachmentData) 
     845            # trac.addAttachment(bugid, tracAttachment, attachment['description'], author) 
     846 
     847            try: 
     848                try: 
     849                    if(os.path.isdir(trac.get_attachments_dir(bugid)) == False): 
     850                        try: 
     851                            os.mkdir(trac.get_attachments_dir(bugid)) 
     852                        except: 
     853                            errorStr = " * ERROR: couldnt create attachment directory in filesystem at %s" % trac.get_attachments_dir(bugid) 
     854                            errors.append(errorStr) 
     855                            print errorStr 
     856                    # trac stores the files with the special characters like spaces in the filename encoded to the url  
     857                    # equivalents, so we have to urllib.quote() the filename we're saving.  
     858                    attachmentFile = open(trac.get_attachments_dir(bugid) + quote(attachment['filename']),'wb') 
     859                    attachmentFile.write(attachment['content']) 
     860                    attachmentFile.close() 
     861                except: 
     862                    errorStr = " * ERROR: couldnt dump attachment data into filesystem at %s" % trac.get_attachments_dir(bugid) + attachment['filename'] 
     863                    errors.append(errorStr) 
     864                    print errorStr 
     865                else: 
     866                    attach_sql = """INSERT INTO attachment (type,id,filename,size,time,description,author,ipnr) VALUES ('ticket',%s,'%s',%i,%i,'%s','%s','127.0.0.1')""" % (bugid,attachment['filename'],attachment['filesize'],attachment['date_added'],attachment['description'],author) 
     867                    try: 
     868                        c = trac.db().cursor() 
     869                        c.execute(attach_sql) 
     870                        trac.db().commit() 
     871                    except: 
     872                        errorStr = " * ERROR: couldnt insert attachment data into database with %s" % attach_sql 
     873                        errors.append(errorStr) 
     874                        print errorStr 
     875                    else: 
     876                        print 'inserting attachment for ticket %s -- %s, added by %s' % (bugid, attachment['description'], author) 
     877 
     878                        totalAttachments += 1 
     879            except: 
     880                errorStr = " * ERROR: couldn't migrate attachment %s" % attachment['filename'] 
     881                errors.append(errorStr) 
     882                print errorStr 
     883 
     884    print 
     885    if TIME_ADJUSTMENT_HACK: 
     886        for adjustment in timeAdjustmentHacks: 
     887            print adjustment 
     888    if len(errors) != 0: 
     889        print "Some errors occurred while importing:" 
     890        for error in errors: 
     891            print error 
     892    else:  
     893        print "Success!" 
     894    print 
     895    print "Total tickets imported: %d" % len(bugs) 
     896    print "Total ticket comments:  %d" % totalComments 
     897    print "Total ticket changes:   %d" % totalTicketChanges 
     898    print "Total attachments:      %d" % totalAttachments 
     899    print 
     900 
     901def usage(): 
     902    print "mantis2trac - Imports a bug database from Mantis into Trac." 
     903    print 
     904    print "Usage: mantis2trac.py [options]" 
     905    print 
     906    print "Available Options:" 
     907    print "  --db <MySQL dbname>              - Mantis database" 
     908    print "  --tracenv /path/to/trac/env/     - Full path to Trac environment" 
     909    print "  -h | --host <MySQL hostname>     - Mantis DNS host name" 
     910    print "  -u | --user <MySQL username>     - Effective Mantis database user" 
     911    print "  -p | --passwd <MySQL password>   - Mantis database user password" 
     912    print "  -c | --clean                     - Remove current Trac tickets before importing" 
     913    print "  --products <product1,product2>   - List of products to import from mantis" 
     914    print "  --help | help                    - This help info" 
     915    print 
     916    print "Note:   If you want the ticket attachments to be converted, you MUST run the script" 
     917    print "        as a user who has write permissions to the trac env attachments directory." 
     918    print "Note 2: Attachment conversion only works for attachments stored directly in the mantis" 
     919    print "        database at this point." 
     920    print 
     921    print "Additional configuration options can be defined directly in the script." 
     922    print 
     923    sys.exit(0) 
     924 
     925def main(): 
     926    global MANTIS_DB, MANTIS_HOST, MANTIS_USER, MANTIS_PASSWORD, TRAC_ENV, TRAC_CLEAN, PRODUCTS 
     927    if len (sys.argv) > 1: 
     928        if sys.argv[1] in ['--help','help'] or len(sys.argv) < 4: 
     929            usage() 
     930        iter = 1 
     931        while iter < len(sys.argv): 
     932            if sys.argv[iter] in ['--db'] and iter+1 < len(sys.argv): 
     933                MANTIS_DB = sys.argv[iter+1] 
     934                iter = iter + 1 
     935            elif sys.argv[iter] in ['-h', '--host'] and iter+1 < len(sys.argv): 
     936                MANTIS_HOST = sys.argv[iter+1] 
     937                iter = iter + 1 
     938            elif sys.argv[iter] in ['-u', '--user'] and iter+1 < len(sys.argv): 
     939                MANTIS_USER = sys.argv[iter+1] 
     940                iter = iter + 1 
     941            elif sys.argv[iter] in ['-p', '--passwd'] and iter+1 < len(sys.argv): 
     942                MANTIS_PASSWORD = sys.argv[iter+1] 
     943                iter = iter + 1 
     944            elif sys.argv[iter] in ['--tracenv'] and iter+1 < len(sys.argv): 
     945                TRAC_ENV = sys.argv[iter+1] 
     946                iter = iter + 1 
     947            elif sys.argv[iter] in ['-c', '--clean']: 
     948                TRAC_CLEAN = 1 
     949            elif sys.argv[iter] in ['--products'] and iter+1 < len(sys.argv): 
     950                PRODUCTS = sys.argv[iter+1].split(',') 
     951                iter = iter + 1 
     952            else: 
     953                print "Error: unknown parameter: " + sys.argv[iter] 
     954                sys.exit(0) 
     955            iter = iter + 1 
     956    else: 
     957        usage() 
     958         
     959    convert(MANTIS_DB, MANTIS_HOST, MANTIS_USER, MANTIS_PASSWORD, TRAC_ENV, TRAC_CLEAN) 
     960 
     961if __name__ == '__main__': 
     962    main()