7
7
Python 2.3 from http://www.python.org/
8
8
MySQL >= 3.23 from http://www.mysql.org/
10
Thanks: Mark Rowe <mrowe@bluewire.net.nz>
10
Thanks: Mark Rowe <mrowe@bluewire.net.nz>
11
11
for original TracDatabase class
13
13
Copyright 2004, Dmitry Yusupov <dmitry_yus@yahoo.com>
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>
28
28
# Bugzilla version. You can find this in Bugzilla's globals.pl file.
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)
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
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.
45
45
# Path to the Trac environment.
46
46
TRAC_ENV = "/usr/local/trac"
48
# If true, all existing Trac tickets and attachments will be removed
48
# If true, all existing Trac tickets and attachments will be removed
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
165
165
"released": "closed"
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())
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):
220
220
return dict.__getitem__(self, item)
222
222
statusXlator = FieldTranslator(STATUS_TRANSLATE)
240
240
def assertNoTickets(self):
241
241
if self.hasTickets():
242
242
raise Exception("Will not modify database with existing tickets!")
244
244
def setSeverityList(self, s):
245
245
"""Remove all severities, set them to `s`"""
246
246
self.assertNoTickets()
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()
257
257
def setPriorityList(self, s):
258
258
"""Remove all priorities, set them to `s`"""
259
259
self.assertNoTickets()
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()
271
271
def setComponentList(self, l, key):
272
272
"""Remove all components, set them to `l`"""
273
273
self.assertNoTickets()
275
275
c = self.db().cursor()
276
276
c.execute("DELETE FROM component")
281
281
(comp[key].encode('utf-8'),
282
282
comp['owner'].encode('utf-8')))
283
283
self.db().commit()
285
285
def setVersionList(self, v, key):
286
286
"""Remove all versions, set them to `v`"""
287
287
self.assertNoTickets()
289
289
c = self.db().cursor()
290
290
c.execute("DELETE FROM version")
293
293
c.execute("INSERT INTO version (name) VALUES (%s)",
294
294
(vers[key].encode('utf-8'),))
295
295
self.db().commit()
297
297
def setMilestoneList(self, m, key):
298
298
"""Remove all milestones, set them to `m`"""
299
299
self.assertNoTickets()
301
301
c = self.db().cursor()
302
302
c.execute("DELETE FROM milestone")
306
306
c.execute("INSERT INTO milestone (name) VALUES (%s)",
307
307
(milestone.encode('utf-8'),))
308
308
self.db().commit()
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()
315
315
desc = description.encode('utf-8')
318
318
if severity.lower() == "enhancement":
319
319
severity = "minor"
320
320
type = "enhancement"
322
322
if PREFORMAT_COMMENTS:
323
323
desc = '{{{\n%s\n}}}' % desc
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,
348
348
self.db().commit()
349
349
return self.db().get_last_id(c, 'ticket')
351
351
def addTicketComment(self, ticket, time, author, value):
352
352
comment = value.encode('utf-8')
354
354
if PREFORMAT_COMMENTS:
355
355
comment = '{{{\n%s\n}}}' % comment
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()
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:
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()
394
394
def addAttachment(self, author, a):
395
395
description = a['description'].encode('utf-8')
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))
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))
428
428
def getFieldName(self, cursor, fieldid):
429
429
if fieldid not in self.fieldNameCache:
430
cursor.execute("SELECT * FROM fielddefs WHERE fieldid = %s",
430
# fielddefs.fieldid got changed to fielddefs.id in Bugzilla
432
if BZ_VERSION >= 2233:
433
cursor.execute("SELECT * FROM fielddefs WHERE id = %s",
436
cursor.execute("SELECT * FROM fielddefs WHERE fieldid = %s",
432
438
fieldName = cursor.fetchall()
449
455
connector, op = ' AND ', '!='
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
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()
479
486
c = trac.db().cursor()
480
487
c.execute("DELETE FROM ticket_change")
481
488
trac.db().commit()
483
490
c.execute("DELETE FROM ticket")
484
491
trac.db().commit()
486
493
c.execute("DELETE FROM attachment")
487
494
attachments_dir = os.path.join(os.path.normpath(trac.env.path),
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")
512
mysql_cur.execute("SELECT product AS name FROM products")
504
514
for line in mysql_cur.fetchall():
505
515
product = line['name']
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)
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)
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')
534
sql = """SELECT program AS product, value AS comp, initialowner AS owner
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))
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)
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')
640
658
ticket['summary'] = bug['short_desc']
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())
645
663
# check for empty 'longdescs[0]' field...
654
672
for comment in IGNORE_COMMENTS:
655
673
if re.match(comment, desc['thetext']):
661
679
trac.addTicketComment(ticket=bugid,
662
680
time = desc['bug_when'],
663
681
author=trac.getLoginName(mysql_cur, desc['who']),
672
690
for activity in bugs_activity:
673
691
field_name = trac.getFieldName(mysql_cur, activity['fieldid']).lower()
675
693
removed = activity[activityFields['removed']]
676
694
added = activity[activityFields['added']]
754
772
# Skip changes that have no effect (think translation!).
755
773
if added == removed:
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']
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)
803
ticket['keywords'] = string.join(keywords)
821
ticket['keywords'] = string.join(keywords)
804
822
ticketid = trac.addTicket(**ticket)
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)
830
mysql_cur.execute("SELECT * FROM attachments WHERE bug_id = %s" %
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)
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")
876
905
print "Error: unknown parameter: " + sys.argv[iter]
880
909
convert(BZ_DB, BZ_HOST, BZ_USER, BZ_PASSWORD, TRAC_ENV, TRAC_CLEAN)
882
911
if __name__ == '__main__':