~ubuntu-branches/ubuntu/jaunty/trac/jaunty

« back to all changes in this revision

Viewing changes to contrib/bugzilla2trac.py

  • Committer: Bazaar Package Importer
  • Author(s): Luis Matos
  • Date: 2008-07-13 23:46:20 UTC
  • mfrom: (1.1.13 upstream)
  • Revision ID: james.westby@ubuntu.com-20080713234620-13ynpdpkbaymfg1z
Tags: 0.11-2
* Re-added python-setup-tools to build dependences. Closes: #490320 #468705
* New upstream release Closes: 489727
* Added sugestion for other vcs support available: git bazaar mercurial 
* Added spamfilter plugin to sugests
* Moved packaging from python-support to python-central
* Added an entry to the NEWS about the cgi Closes: #490275
* Updated 10_remove_trac_suffix_from_title patch to be used in 0.11

Show diffs side-by-side

added added

removed removed

Lines of Context:
7
7
           Python 2.3 from http://www.python.org/
8
8
           MySQL >= 3.23 from http://www.mysql.org/
9
9
 
10
 
Thanks:    Mark Rowe <mrowe@bluewire.net.nz> 
 
10
Thanks:    Mark Rowe <mrowe@bluewire.net.nz>
11
11
            for original TracDatabase class
12
 
           
 
12
 
13
13
Copyright 2004, Dmitry Yusupov <dmitry_yus@yahoo.com>
14
14
 
15
15
Many enhancements, Bill Soudan <bill@soudan.net>
16
16
Other enhancements, Florent Guillaume <fg@nuxeo.com>
17
 
Reworked, Jeroen Ruigrok van der Werven <asmodai@tendra.org>
 
17
Reworked, Jeroen Ruigrok van der Werven <asmodai@in-nomine.org>
18
18
 
19
19
$Id$
20
20
"""
28
28
# Bugzilla version.  You can find this in Bugzilla's globals.pl file.
29
29
#
30
30
# Currently, the following bugzilla versions are known to work:
31
 
#   2.11 (2110), 2.16.5 (2165), 2.18.3 (2183), 2.19.1 (2191)
 
31
#   2.11 (2110), 2.16.5 (2165), 2.18.3 (2183), 2.19.1 (2191), 2.23.3 (2233)
32
32
#
33
33
# If you run this script on a version not listed here and it is successful,
34
 
# please report it to the Trac mailing list and drop a note to
35
 
# asmodai@tendra.org so we can update the list.
 
34
# please file a ticket at http://trac.edgewall.org/ and assign it to
 
35
# jruigrok.
36
36
BZ_VERSION = 2180
37
37
 
38
 
# MySQL connection parameters for the Bugzilla database.  These can also 
 
38
# MySQL connection parameters for the Bugzilla database.  These can also
39
39
# be specified on the command line.
40
40
BZ_DB = ""
41
41
BZ_HOST = ""
45
45
# Path to the Trac environment.
46
46
TRAC_ENV = "/usr/local/trac"
47
47
 
48
 
# If true, all existing Trac tickets and attachments will be removed 
 
48
# If true, all existing Trac tickets and attachments will be removed
49
49
# prior to import.
50
50
TRAC_CLEAN = True
51
51
 
52
 
# Enclose imported ticket description and comments in a {{{ }}} 
 
52
# Enclose imported ticket description and comments in a {{{ }}}
53
53
# preformat block?  This formats the text in a fixed-point font.
54
54
PREFORMAT_COMMENTS = False
55
55
 
165
165
  "released":    "closed"
166
166
}
167
167
 
168
 
# Translate Bugzilla statuses into Trac keywords.  This provides a way 
169
 
# to retain the Bugzilla statuses in Trac.  e.g. when a bug is marked 
 
168
# Translate Bugzilla statuses into Trac keywords.  This provides a way
 
169
# to retain the Bugzilla statuses in Trac.  e.g. when a bug is marked
170
170
# 'verified' in Bugzilla it will be assigned a VERIFIED keyword.
171
171
STATUS_KEYWORDS = {
172
172
  "verified": "VERIFIED",
209
209
#    def __init__(self, name, data):
210
210
#        self.filename = name
211
211
#        self.file = StringIO.StringIO(data.tostring())
212
 
  
 
212
 
213
213
# simple field translation mapping.  if string not in
214
214
# mapping, just return string, otherwise return value
215
215
class FieldTranslator(dict):
216
216
    def __getitem__(self, item):
217
217
        if not dict.has_key(self, item):
218
218
            return item
219
 
            
 
219
 
220
220
        return dict.__getitem__(self, item)
221
221
 
222
222
statusXlator = FieldTranslator(STATUS_TRANSLATE)
228
228
        self._db.autocommit = False
229
229
        self.loginNameCache = {}
230
230
        self.fieldNameCache = {}
231
 
    
 
231
 
232
232
    def db(self):
233
233
        return self._db
234
 
    
 
234
 
235
235
    def hasTickets(self):
236
236
        c = self.db().cursor()
237
237
        c.execute("SELECT count(*) FROM Ticket")
240
240
    def assertNoTickets(self):
241
241
        if self.hasTickets():
242
242
            raise Exception("Will not modify database with existing tickets!")
243
 
    
 
243
 
244
244
    def setSeverityList(self, s):
245
245
        """Remove all severities, set them to `s`"""
246
246
        self.assertNoTickets()
247
 
        
 
247
 
248
248
        c = self.db().cursor()
249
249
        c.execute("DELETE FROM enum WHERE type='severity'")
250
250
        for value, i in s:
253
253
                                   VALUES (%s, %s, %s)""",
254
254
                      ("severity", value.encode('utf-8'), i))
255
255
        self.db().commit()
256
 
    
 
256
 
257
257
    def setPriorityList(self, s):
258
258
        """Remove all priorities, set them to `s`"""
259
259
        self.assertNoTickets()
260
 
        
 
260
 
261
261
        c = self.db().cursor()
262
262
        c.execute("DELETE FROM enum WHERE type='priority'")
263
263
        for value, i in s:
267
267
                      ("priority", value.encode('utf-8'), i))
268
268
        self.db().commit()
269
269
 
270
 
    
 
270
 
271
271
    def setComponentList(self, l, key):
272
272
        """Remove all components, set them to `l`"""
273
273
        self.assertNoTickets()
274
 
        
 
274
 
275
275
        c = self.db().cursor()
276
276
        c.execute("DELETE FROM component")
277
277
        for comp in l:
281
281
                      (comp[key].encode('utf-8'),
282
282
                       comp['owner'].encode('utf-8')))
283
283
        self.db().commit()
284
 
    
 
284
 
285
285
    def setVersionList(self, v, key):
286
286
        """Remove all versions, set them to `v`"""
287
287
        self.assertNoTickets()
288
 
        
 
288
 
289
289
        c = self.db().cursor()
290
290
        c.execute("DELETE FROM version")
291
291
        for vers in v:
293
293
            c.execute("INSERT INTO version (name) VALUES (%s)",
294
294
                      (vers[key].encode('utf-8'),))
295
295
        self.db().commit()
296
 
        
 
296
 
297
297
    def setMilestoneList(self, m, key):
298
298
        """Remove all milestones, set them to `m`"""
299
299
        self.assertNoTickets()
300
 
        
 
300
 
301
301
        c = self.db().cursor()
302
302
        c.execute("DELETE FROM milestone")
303
303
        for ms in m:
306
306
            c.execute("INSERT INTO milestone (name) VALUES (%s)",
307
307
                      (milestone.encode('utf-8'),))
308
308
        self.db().commit()
309
 
    
 
309
 
310
310
    def addTicket(self, id, time, changetime, component, severity, priority,
311
311
                  owner, reporter, cc, version, milestone, status, resolution,
312
312
                  summary, description, keywords):
313
313
        c = self.db().cursor()
314
 
        
 
314
 
315
315
        desc = description.encode('utf-8')
316
316
        type = "defect"
317
 
        
 
317
 
318
318
        if severity.lower() == "enhancement":
319
319
                severity = "minor"
320
320
                type = "enhancement"
321
 
        
 
321
 
322
322
        if PREFORMAT_COMMENTS:
323
323
          desc = '{{{\n%s\n}}}' % desc
324
324
 
338
338
                                         keywords)
339
339
                                 VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s,
340
340
                                         %s, %s, %s, %s, %s, %s, %s, %s)""",
341
 
                  (id, type.encode('utf-8'), time.strftime('%s'),
342
 
                   changetime.strftime('%s'), component.encode('utf-8'),
 
341
                  (id, type.encode('utf-8'), datetime2epoch(time),
 
342
                   datetime2epoch(changetime), component.encode('utf-8'),
343
343
                   severity.encode('utf-8'), priority.encode('utf-8'), owner,
344
344
                   reporter, cc, version, milestone.encode('utf-8'),
345
345
                   status.lower(), resolution, summary.encode('utf-8'), desc,
346
346
                   keywords))
347
 
        
 
347
 
348
348
        self.db().commit()
349
349
        return self.db().get_last_id(c, 'ticket')
350
 
    
 
350
 
351
351
    def addTicketComment(self, ticket, time, author, value):
352
352
        comment = value.encode('utf-8')
353
 
        
 
353
 
354
354
        if PREFORMAT_COMMENTS:
355
355
          comment = '{{{\n%s\n}}}' % comment
356
356
 
362
362
        c.execute("""INSERT INTO ticket_change (ticket, time, author, field,
363
363
                                                oldvalue, newvalue)
364
364
                                        VALUES (%s, %s, %s, %s, %s, %s)""",
365
 
                  (ticket, time.strftime('%s'), author, 'comment', '', comment))
 
365
                  (ticket, datetime2epoch(time), author, 'comment', '', comment))
366
366
        self.db().commit()
367
367
 
368
368
    def addTicketChange(self, ticket, time, author, field, oldvalue, newvalue):
383
383
        # Doesn't make sense if we go from highest -> highest, for example.
384
384
        if oldvalue == newvalue:
385
385
            return
386
 
        
 
386
 
387
387
        c.execute("""INSERT INTO ticket_change (ticket, time, author, field,
388
388
                                                oldvalue, newvalue)
389
389
                                        VALUES (%s, %s, %s, %s, %s, %s)""",
390
 
                  (ticket, time.strftime('%s'), author, field,
 
390
                  (ticket, datetime2epoch(time), author, field,
391
391
                   oldvalue.encode('utf-8'), newvalue.encode('utf-8')))
392
392
        self.db().commit()
393
 
        
 
393
 
394
394
    def addAttachment(self, author, a):
395
395
        description = a['description'].encode('utf-8')
396
396
        id = a['bug_id']
397
397
        filename = a['filename'].encode('utf-8')
398
 
        filedata = StringIO.StringIO(a['thedata'].tostring())
 
398
        filedata = StringIO.StringIO(a['thedata'])
399
399
        filesize = len(filedata.getvalue())
400
400
        time = a['creation_ts']
401
401
        print "    ->inserting attachment '%s' for ticket %s -- %s" % \
404
404
        attachment = Attachment(self.env, 'ticket', id)
405
405
        attachment.author = author
406
406
        attachment.description = description
407
 
        attachment.insert(filename, filedata, filesize, time.strftime('%s'))
 
407
        attachment.insert(filename, filedata, filesize, datetime2epoch(time))
408
408
        del attachment
409
 
        
 
409
 
410
410
    def getLoginName(self, cursor, userid):
411
411
        if userid not in self.loginNameCache:
412
412
            cursor.execute("SELECT * FROM profiles WHERE userid = %s", (userid))
427
427
 
428
428
    def getFieldName(self, cursor, fieldid):
429
429
        if fieldid not in self.fieldNameCache:
430
 
            cursor.execute("SELECT * FROM fielddefs WHERE fieldid = %s",
431
 
                           (fieldid))
 
430
            # fielddefs.fieldid got changed to fielddefs.id in Bugzilla
 
431
            # 2.23.3.
 
432
            if BZ_VERSION >= 2233:
 
433
                cursor.execute("SELECT * FROM fielddefs WHERE id = %s",
 
434
                               (fieldid))
 
435
            else:
 
436
                cursor.execute("SELECT * FROM fielddefs WHERE fieldid = %s",
 
437
                               (fieldid))
432
438
            fieldName = cursor.fetchall()
433
439
 
434
440
            if fieldName:
449
455
        connector, op = ' AND ', '!='
450
456
    else:
451
457
        connector, op = ' OR ', '='
452
 
    clause = connector.join(["%s %s '%s'" % (fieldName, op, value) for value in values])
 
458
    clause = connector.join(["%s %s '%s'" % (fieldName, op, value) 
 
459
                             for value in values])
453
460
    return ' ' + clause
454
461
 
455
462
def convert(_db, _host, _user, _password, _env, _force):
464
471
    # init Bugzilla environment
465
472
    print "Bugzilla MySQL('%s':'%s':'%s':'%s'): connecting..." % \
466
473
            (_db, _host, _user, ("*" * len(_password)))
467
 
    mysql_con = MySQLdb.connect(host=_host, 
468
 
                user=_user, passwd=_password, db=_db, compress=1, 
 
474
    mysql_con = MySQLdb.connect(host=_host,
 
475
                user=_user, passwd=_password, db=_db, compress=1,
469
476
                cursorclass=MySQLdb.cursors.DictCursor)
470
477
    mysql_cur = mysql_con.cursor()
471
478
 
479
486
        c = trac.db().cursor()
480
487
        c.execute("DELETE FROM ticket_change")
481
488
        trac.db().commit()
482
 
        
 
489
 
483
490
        c.execute("DELETE FROM ticket")
484
491
        trac.db().commit()
485
 
        
 
492
 
486
493
        c.execute("DELETE FROM attachment")
487
494
        attachments_dir = os.path.join(os.path.normpath(trac.env.path),
488
495
                                "attachments")
499
506
 
500
507
 
501
508
    print "\n0. Filtering products..."
502
 
    mysql_cur.execute("SELECT name FROM products")
 
509
    if BZ_VERSION >= 2180:
 
510
        mysql_cur.execute("SELECT name FROM products")
 
511
    else:
 
512
        mysql_cur.execute("SELECT product AS name FROM products")
503
513
    products = []
504
514
    for line in mysql_cur.fetchall():
505
515
        product = line['name']
516
526
 
517
527
    print "\n2. Import components..."
518
528
    if not COMPONENTS_FROM_PRODUCTS:
519
 
        if BZ_VERSION >= 2180:
520
 
            sql = """SELECT DISTINCT c.name AS name, c.initialowner AS owner
521
 
                                FROM components AS c, products AS p
522
 
                               WHERE c.product_id = p.id AND"""
523
 
            sql += makeWhereClause('p.name', PRODUCTS)
524
 
        else:
525
 
            sql = "SELECT value AS name, initialowner AS owner FROM components"
 
529
        if BZ_VERSION >= 2180:
 
530
            sql = """SELECT DISTINCT c.name AS name, c.initialowner AS owner
 
531
                               FROM components AS c, products AS p
 
532
                               WHERE c.product_id = p.id AND"""
 
533
            sql += makeWhereClause('p.name', PRODUCTS)
 
534
        else:
 
535
            sql = "SELECT value AS name, initialowner AS owner FROM components"
526
536
            sql += " WHERE" + makeWhereClause('program', PRODUCTS)
527
537
        mysql_cur.execute(sql)
528
538
        components = mysql_cur.fetchall()
531
541
                                                   component['owner'])
532
542
        trac.setComponentList(components, 'name')
533
543
    else:
534
 
        sql = """SELECT program AS product, value AS comp, initialowner AS owner
535
 
                   FROM components"""
536
 
        sql += " WHERE" + makeWhereClause('program', PRODUCTS)
 
544
        if BZ_VERSION >= 2180:
 
545
            sql = ("SELECT p.name AS product, c.name AS comp, "
 
546
                   " c.initialowner AS owner "
 
547
                   "FROM components c, products p "
 
548
                   "WHERE c.product_id = p.id and " + 
 
549
                   makeWhereClause('p.name', PRODUCTS))
 
550
        else:
 
551
            sql = ("SELECT program AS product, value AS comp, "
 
552
                   " initialowner AS owner "
 
553
                   "FROM components WHERE" + 
 
554
                   makeWhereClause('program', PRODUCTS))
537
555
        mysql_cur.execute(sql)
538
556
        lines = mysql_cur.fetchall()
539
557
        all_components = {} # product -> components
566
584
    if BZ_VERSION >= 2180:
567
585
        sql = """SELECT DISTINCTROW versions.value AS value
568
586
                               FROM products, versions"""
569
 
        sql += " WHERE" + makeWhereClause('products.name', PRODUCTS)
 
587
        sql += " WHERE" + makeWhereClause('products.name', PRODUCTS)
570
588
    else:
571
 
        sql = "SELECT DISTINCTROW value FROM versions"
572
 
        sql += " WHERE" + makeWhereClause('program', PRODUCTS)
 
589
        sql = "SELECT DISTINCTROW value FROM versions"
 
590
        sql += " WHERE" + makeWhereClause('program', PRODUCTS)
573
591
    mysql_cur.execute(sql)
574
592
    versions = mysql_cur.fetchall()
575
593
    trac.setVersionList(versions, 'value')
591
609
    mysql_cur.execute(sql)
592
610
    bugs = mysql_cur.fetchall()
593
611
 
594
 
    
 
612
 
595
613
    print "\n7. Import bugs and bug activity..."
596
614
    for bug in bugs:
597
615
        bugid = bug['bug_id']
598
 
        
 
616
 
599
617
        ticket = {}
600
618
        keywords = []
601
619
        ticket['id'] = bugid
639
657
 
640
658
        ticket['summary'] = bug['short_desc']
641
659
 
642
 
        mysql_cur.execute("SELECT * FROM longdescs WHERE bug_id = %s" % bugid) 
 
660
        mysql_cur.execute("SELECT * FROM longdescs WHERE bug_id = %s" % bugid)
643
661
        longdescs = list(mysql_cur.fetchall())
644
662
 
645
663
        # check for empty 'longdescs[0]' field...
654
672
            for comment in IGNORE_COMMENTS:
655
673
                if re.match(comment, desc['thetext']):
656
674
                    ignore = True
657
 
                    
 
675
 
658
676
            if ignore:
659
677
                    continue
660
 
            
 
678
 
661
679
            trac.addTicketComment(ticket=bugid,
662
680
                time = desc['bug_when'],
663
681
                author=trac.getLoginName(mysql_cur, desc['who']),
671
689
        keywords = []
672
690
        for activity in bugs_activity:
673
691
            field_name = trac.getFieldName(mysql_cur, activity['fieldid']).lower()
674
 
            
 
692
 
675
693
            removed = activity[activityFields['removed']]
676
694
            added = activity[activityFields['added']]
677
695
 
754
772
            # Skip changes that have no effect (think translation!).
755
773
            if added == removed:
756
774
                continue
757
 
                
 
775
 
758
776
            # Bugzilla splits large summary changes into two records.
759
777
            for oldChange in ticketChanges:
760
778
              if (field_name == "summary"
761
 
                  and oldChange['field'] == ticketChange['field'] 
762
 
                  and oldChange['time'] == ticketChange['time'] 
 
779
                  and oldChange['field'] == ticketChange['field']
 
780
                  and oldChange['time'] == ticketChange['time']
763
781
                  and oldChange['author'] == ticketChange['author']):
764
 
                  oldChange['oldvalue'] += " " + ticketChange['oldvalue'] 
 
782
                  oldChange['oldvalue'] += " " + ticketChange['oldvalue']
765
783
                  oldChange['newvalue'] += " " + ticketChange['newvalue']
766
784
                  break
767
785
              # cc sometime appear in different activities with same time
800
818
            if kw and kw not in keywords:
801
819
                keywords.append(kw)
802
820
 
803
 
        ticket['keywords'] = string.join(keywords)                
 
821
        ticket['keywords'] = string.join(keywords)
804
822
        ticketid = trac.addTicket(**ticket)
805
823
 
806
 
        mysql_cur.execute("SELECT * FROM attachments WHERE bug_id = %s" % bugid)
 
824
        if BZ_VERSION >= 2180:
 
825
            mysql_cur.execute("SELECT attachments.*, attach_data.thedata "
 
826
                              "FROM attachments, attach_data "
 
827
                              "WHERE attachments.bug_id = %s AND "
 
828
                              "attachments.attach_id = attach_data.id" % bugid)
 
829
        else:
 
830
            mysql_cur.execute("SELECT * FROM attachments WHERE bug_id = %s" % 
 
831
                              bugid)
807
832
        attachments = mysql_cur.fetchall()
808
833
        for a in attachments:
809
834
            author = trac.getLoginName(mysql_cur, a['submitter_id'])
810
835
            trac.addAttachment(author, a)
811
 
            
 
836
 
812
837
    print "\n8. Importing users and passwords..."
813
838
    if BZ_VERSION >= 2180:
814
839
        mysql_cur.execute("SELECT login_name, cryptpassword FROM profiles")
829
854
def log(msg):
830
855
    print "DEBUG: %s" % (msg)
831
856
 
 
857
def datetime2epoch(dt) :
 
858
    import time
 
859
    return time.mktime(dt.timetuple())
 
860
 
832
861
def usage():
833
862
    print """bugzilla2trac - Imports a bug database from Bugzilla into Trac.
834
863
 
876
905
                print "Error: unknown parameter: " + sys.argv[iter]
877
906
                sys.exit(0)
878
907
            iter = iter + 1
879
 
        
 
908
 
880
909
    convert(BZ_DB, BZ_HOST, BZ_USER, BZ_PASSWORD, TRAC_ENV, TRAC_CLEAN)
881
910
 
882
911
if __name__ == '__main__':