Commits

daniel  committed ed2e751

Added SourceForge and Bugzilla converters from Mark Rowe and Dmitry Yusupov.
Many thanks, guys! Fixes #328 and #224.

  • Participants
  • Parent commits 56112b9
  • Branches trunk

Comments (0)

Files changed (4)

 Francois Harvey         fharvey@securiweb.net
 Richard Hult            richard@imendio.com
 Nuutti Kotivuori        naked@iki.fi
+Christopher Lenz        cmlenz@gmx.de
 Ivo Looser              ivo.looser@login.ch
 Jennifer Murtell        jen@jmurtell.com
 Nicolas Riley           sabi
 Olliver Rutherfurd      ollie
 Andreas Schrattenecker  vittorio
 Emmeran Seehuber        rototor@rototor.de
+Dmitry Yusupov          dmitry_yus@yahoo.com
 
 The Elusive Anonymous
 

File contrib/README

+This directory contains useful contributed scripts and programs for Trac.
+
+Please note that these scripts are provided AS-IS are NOT tested to the same
+extent as the main sources. They are usually maintained by the original
+author, typically listed at the top of the source file.
+
+See ../THANKS for a list of contributors.

File contrib/bugzilla2trac.py

+"""
+Import a Bugzilla items into a Trac database.
+
+Requires:  Development version of Trac 0.7-pre from http://trac.edgewall.com/
+           Python 2.3 from http://www.python.org/
+	   MySQL >= 3.23 from http://www.mysql.org/
+
+Thanks:    Mark Rowe <mrowe@bluewire.net.nz> 
+			for original TracDatabase class
+           
+Copyright 2004, Dmitry Yusupov <dmitry_yus@yahoo.com>
+"""
+
+import sys
+import MySQLdb
+import MySQLdb.cursors
+import trac.Environment
+
+class TracDatabase(object):
+    def __init__(self, path):
+        self.env = trac.Environment.Environment(path)
+        self._db = self.env.get_db_cnx()
+        self._db.autocommit = False
+    
+    def db(self):
+        return self._db
+    
+    def hasTickets(self):
+        c = self.db().cursor()
+        c.execute('''SELECT count(*) FROM Ticket''')
+        return int(c.fetchall()[0][0]) > 0
+    
+    def setSeverityList(self, s):
+        """Remove all severities, set them to `s`"""
+        if self.hasTickets():
+            raise Exception("Will not modify database with existing tickets!")
+        
+        c = self.db().cursor()
+        c.execute("""DELETE FROM enum WHERE type='severity'""")
+        for value, i in s:
+            print "inserting severity ", value, " ", i
+            c.execute("""INSERT INTO enum (type, name, value) VALUES (%s, %s, %s)""",
+                      "severity", value, i)
+        self.db().commit()
+    
+    def setPriorityList(self, s):
+        """Remove all priorities, set them to `s`"""
+        if self.hasTickets():
+            raise Exception("Will not modify database with existing tickets!")
+        
+        c = self.db().cursor()
+        c.execute("""DELETE FROM enum WHERE type='priority'""")
+        for value, i in s:
+            print "inserting priority ", value, " ", i
+            c.execute("""INSERT INTO enum (type, name, value) VALUES (%s, %s, %s)""",
+                      "priority",
+                      value,
+                      i)
+        self.db().commit()
+
+    
+    def setComponentList(self, l, key):
+        """Remove all components, set them to `l`"""
+        if self.hasTickets():
+            raise Exception("Will not modify database with existing tickets!")
+        
+        c = self.db().cursor()
+        c.execute("""DELETE FROM component""")
+        for comp in l:
+            print "inserting component ", comp[key]
+            c.execute("""INSERT INTO component (name) VALUES (%s)""",
+                      comp[key])
+        self.db().commit()
+    
+    def setVersionList(self, v, key):
+        """Remove all versions, set them to `v`"""
+        if self.hasTickets():
+            raise Exception("Will not modify database with existing tickets!")
+        
+        c = self.db().cursor()
+        c.execute("""DELETE FROM version""")
+        for vers in v:
+            print "inserting version ", vers[key]
+            c.execute("""INSERT INTO version (name) VALUES (%s)""",
+                      vers[key])
+        self.db().commit()
+        
+    def setMilestoneList(self, m, key):
+        """Remove all milestones, set them to `m`"""
+        if self.hasTickets():
+            raise Exception("Will not modify database with existing tickets!")
+        
+        c = self.db().cursor()
+        c.execute("""DELETE FROM milestone""")
+        for ms in m:
+            print "inserting milestone ", ms[key]
+            c.execute("""INSERT INTO milestone (name) VALUES (%s)""",
+                      ms[key])
+        self.db().commit()
+    
+    def addTicket(self, time, changetime, component,
+                  severity, priority, owner, reporter, cc,
+                  version, milestone, status, resolution,
+                  summary, description, keywords):
+        c = self.db().cursor()
+
+        c.execute("""INSERT INTO ticket (time, changetime, component,
+                                         severity, priority, owner, reporter, cc,
+                                         version, milestone, status, resolution,
+                                         summary, description, keywords)
+                                 VALUES (%s, %s, %s,
+                                         %s, %s, %s, %s, %s,
+                                         %s, %s, %s, %s,
+                                         %s, %s, %s)""",
+                  time.strftime('%s'), changetime.strftime('%s'), component,
+                  severity, priority, owner, reporter, cc,
+                  version, milestone, status.lower(), resolution,
+                  summary, '{{{\n%s\n}}}' % (description, ), keywords)
+        self.db().commit()
+        return self.db().db.sqlite_last_insert_rowid()
+    
+    def addTicketComment(self, ticket, time, author, value):
+        c = self.db().cursor()
+        c.execute("""INSERT INTO ticket_change (ticket, time, author, field, oldvalue, newvalue)
+                                 VALUES        (%s, %s, %s, %s, %s, %s)""",
+                  ticket, time.strftime('%s'), author, 'comment', '', '{{{\n%s\n}}}' % (value, ))
+        self.db().commit()
+
+    def addTicketChange(self, ticket, time, author, field, oldvalue, newvalue):
+        c = self.db().cursor()
+        c.execute("""INSERT INTO ticket_change (ticket, time, author, field, oldvalue, newvalue)
+                                 VALUES        (%s, %s, %s, %s, %s, %s)""",
+                  ticket, time.strftime('%s'), author, field, oldvalue, newvalue)
+        self.db().commit()
+
+class TheBugzillaConverter:
+	def __init__(self, _db, _host, _user, _password, _env, _force):
+
+		try:
+			# init Bugzilla environment
+			print "Bugzilla MySQL('%s':'%s':'%s':'%s'): connecting..." % (_db, _host, _user, _password)
+			self.mysql_con = MySQLdb.connect(host=_host, 
+						user=_user, passwd=_password, db=_db, compress=1, 
+						cursorclass=MySQLdb.cursors.DictCursor)
+			self.mysql_cur = self.mysql_con.cursor()
+
+			# init Trac environment
+			print "Trac SQLite('%s'): connecting..." % (_env)
+			trac = TracDatabase(_env)
+
+			# force mode...
+			if _force == 1:
+				print "cleaning all tickets..."
+				c = trac.db().cursor()
+				c.execute("""DELETE FROM ticket_change""")
+				trac.db().commit()
+				c.execute("""DELETE FROM ticket""")
+				trac.db().commit()
+
+			# 1. import all severities...
+			severities = (('blocker', '1'), ('critical', '2'), ('major', '3'), ('normal', '4'),
+				('minor', '5'), ('trivial', '6'), ('enhancement', '7'))
+			trac.setSeverityList(severities)
+
+			# 2. import all components...
+			self.mysql_cur.execute("SELECT value FROM components")
+			components = self.mysql_cur.fetchall()
+			trac.setComponentList(components, 'value')
+
+			# 3. import all priorities...
+			priorities = (('P1', '1'), ('P2', '2'), ('P3', '3'), ('P4', '4'), ('P5', '5'))
+			trac.setPriorityList(priorities)
+
+			# 4. import all versions...
+			self.mysql_cur.execute("SELECT value FROM versions")
+			versions = self.mysql_cur.fetchall()
+			trac.setVersionList(versions, 'value')
+
+			# 5. import all milestones...
+			self.mysql_cur.execute("SELECT value FROM milestones")
+			milestones = self.mysql_cur.fetchall()
+			if milestones[0] == '---':
+				trac.setMilestoneList(milestones, 'value')
+			else:
+				trac.setMilestoneList([], '')
+
+			# 6. import tickets and history...
+			self.mysql_cur.execute("SELECT * FROM bugs ORDER BY bug_id")
+			bugs = self.mysql_cur.fetchall()
+			ticket = {}
+			for bug in bugs:
+				ticket['time'] = bug['creation_ts']
+				ticket['changetime'] = bug['delta_ts']
+				ticket['component'] = bug['component']
+				ticket['severity'] = bug['bug_severity']
+				ticket['priority'] = bug['priority']
+
+				self.mysql_cur.execute("SELECT * FROM profiles WHERE userid = " + 
+								str(bug['assigned_to']))
+				owner = self.mysql_cur.fetchall()
+
+				# check for empty 'owner[0]' field...
+				if len(owner) == 0:
+					ticket['owner'] = ''
+				else:
+					ticket['owner'] = owner[0]['login_name']
+
+				self.mysql_cur.execute("SELECT * FROM profiles WHERE userid = " + 
+								str(bug['reporter']))
+				reporter = self.mysql_cur.fetchall()
+
+				# check for empty 'reporter[0]' field...
+				if len(reporter) == 0:
+					ticket['reporter'] = ''
+				else:
+					ticket['reporter'] = reporter[0]['login_name']
+
+				self.mysql_cur.execute("SELECT * FROM cc WHERE bug_id = " + 
+								str(bug['bug_id']))
+				cc_list = self.mysql_cur.fetchall()
+				cc_str = ''
+				last = 1
+				for cc in cc_list:
+					self.mysql_cur.execute("SELECT * FROM profiles WHERE userid = " + 
+									str(cc['who']))
+					cc_profile = self.mysql_cur.fetchall()
+					cc_str = cc_str + cc_profile[0]['login_name']
+					if len(cc_list) != last:
+						cc_str = cc_str + ', '
+					last = last + 1
+				ticket['cc'] = cc_str
+
+				ticket['version'] = bug['version']
+
+				if bug['target_milestone'] == '---':
+					ticket['milestone'] = ''
+				else:
+					ticket['milestone'] = bug['target_milestone']
+
+				ticket['status'] = bug['bug_status'].lower()
+				ticket['resolution'] = bug['resolution'].lower()
+
+				# convert bugzilla's status field...
+				if ticket['status'] == 'open':
+				    if owner != '':
+					ticket['status'] = 'assigned'
+				    else:
+					ticket['status'] = 'new'
+				elif ticket['status'] == 'review ready':
+				    if ticket['owner'] != '':
+					ticket['status'] = 'assigned'
+				    else:
+					ticket['status'] = 'new'
+				    ticket['resolution'] = 'fixed'
+				elif ticket['status'] == 'review completed':
+					ticket['status'] = 'closed'
+					ticket['resolution'] = 'fixed'
+				elif ticket['status'] == 'review rejected':
+				    if ticket['owner'] != '':
+					ticket['status'] = 'assigned'
+				    else:
+					ticket['status'] = 'new'
+
+				ticket['summary'] = bug['short_desc']
+
+				self.mysql_cur.execute("SELECT * FROM longdescs WHERE bug_id = " + 
+								str(bug['bug_id']))
+				longdescs = self.mysql_cur.fetchall()
+
+				# check for empty 'longdescs[0]' field...
+				if len(longdescs) == 0:
+					ticket['description'] = ''
+				else:
+					ticket['description'] = longdescs[0]['thetext']
+
+				ticket['keywords'] = bug['keywords']
+
+				i = trac.addTicket(time=ticket['time'],
+					changetime=ticket['changetime'],
+					component=ticket['component'],
+					severity=ticket['severity'],
+					priority=ticket['priority'],
+					owner=ticket['owner'],
+					reporter=ticket['reporter'],
+					cc=ticket['cc'],
+					version=ticket['version'],
+					milestone=ticket['milestone'],
+					status=ticket['status'],
+					resolution=ticket['resolution'],
+					summary=ticket['summary'],
+					description=ticket['description'],
+					keywords=ticket['keywords'])
+				iter = 0
+				for desc in longdescs:
+					if iter == 0: 
+						iter = iter + 1
+						continue
+
+					self.mysql_cur.execute("SELECT * FROM profiles WHERE userid = " + 
+								str(desc['who']))
+					who = self.mysql_cur.fetchall()
+
+					# check for empty 'who[0]' field...
+					if len(who) == 0:
+						who_name = ''
+					else:
+						who_name = who[0]['login_name']
+
+					trac.addTicketComment(ticket=i,
+						time=desc['bug_when'],
+						author=who_name,
+						value=desc['thetext'])
+					iter = iter + 1
+
+				self.mysql_cur.execute("SELECT * FROM bugs_activity WHERE bug_id = " + 
+								str(bug['bug_id']))
+				bugs_activity = self.mysql_cur.fetchall()
+				for activity in bugs_activity:
+					self.mysql_cur.execute("SELECT * FROM profiles WHERE userid = " + 
+								str(activity['who']))
+					who = self.mysql_cur.fetchall()
+
+					# check for empty 'who[0]' field...
+					if len(who) == 0:
+						who_name = ''
+					else:
+						who_name = who[0]['login_name']
+
+					self.mysql_cur.execute("SELECT * FROM fielddefs WHERE fieldid = " + 
+								str(activity['fieldid']))
+					field = self.mysql_cur.fetchall()
+
+					# check for empty 'field[0]' field...
+					if len(field) == 0:
+						field_name = ''
+					else:
+						field_name = field[0]['name'].lower()
+
+					removed = activity['removed'].lower()
+					added = activity['added'].lower()
+
+					# convert bugzilla field names...
+					if field_name == 'bug_severity':
+						field_name = 'severity'
+					elif field_name == 'assigned_to':
+						field_name = 'owner'
+					elif field_name == 'bug_status':
+						field_name = 'status'
+						if activity['removed'] == 'review ready':
+							removed = 'assigned'
+						elif activity['removed'] == 'review completed':
+							removed = 'closed'
+						elif activity['removed'] == 'review rejected':
+							removed = 'assigned'
+						if activity['added'] == 'review ready':
+							added = 'assigned'
+						elif activity['added'] == 'review completed':
+							added = 'closed'
+						elif activity['added'] == 'review rejected':
+							added = 'assigned'
+
+					elif field_name.lower() == 'short_desc':
+						field_name = 'summary'
+
+					try: 
+						trac.addTicketChange(ticket=i,
+							time=activity['bug_when'],
+							author=who_name,
+							field=field_name,
+							oldvalue=removed,
+							newvalue=added)
+					except Exception, e:
+						print "bug activity " + str(activity['fieldid']) + " skipped for reason: ", e
+
+				print "inserted ticket ", str(i), " bug_id ", str(bug['bug_id'])
+
+			print "Success!"
+
+		except Exception, e:
+			print 'Error:', e
+
+def usage():
+	print """\
+Usage: bugzilla2trac.py --db <MySQL dbname>              - Bugzilla's database
+                        [-h | --host <MySQL hostname>]   - Bugzilla's DNS host name
+                        [-u | --user <MySQL username>]   - effective Bugzilla's database user
+                        [-p | --passwd <MySQL password>] - Bugzilla's user password
+			--tracenv /path/to/trac/env      - full path to Trac db environment
+                        [-f | --force]                   - force to clean up Trac db
+                        [--help | help]                  - this help info"""
+	sys.exit(0)
+
+if __name__ == "__main__":
+	db = ''
+	host = 'localhost'
+	user = ''
+	password = ''
+	env = ''
+	force = 0
+	if len (sys.argv) > 1:
+		if sys.argv[1] in ['--help','help'] or len(sys.argv) < 4:
+			usage()
+		iter = 1
+		while iter < len(sys.argv):
+			if sys.argv[iter] in ['--db'] and iter+1 < len(sys.argv):
+				db = sys.argv[iter+1]
+				iter = iter + 1
+			elif sys.argv[iter] in ['-h', '--host'] and iter+1 < len(sys.argv):
+				host = sys.argv[iter+1]
+				iter = iter + 1
+			elif sys.argv[iter] in ['-u', '--user'] and iter+1 < len(sys.argv):
+				user = sys.argv[iter+1]
+				iter = iter + 1
+			elif sys.argv[iter] in ['-p', '--passwd'] and iter+1 < len(sys.argv):
+				passwd = sys.argv[iter+1]
+				iter = iter + 1
+			elif sys.argv[iter] in ['--tracenv'] and iter+1 < len(sys.argv):
+				env = sys.argv[iter+1]
+				iter = iter + 1
+			elif sys.argv[iter] in ['-f', '--force']:
+				force = 1
+			else:
+				print "Error: unknown parameter: " + sys.argv[iter]
+				sys.exit(0)
+			iter = iter + 1
+	else:
+		usage()
+	TheBugzillaConverter(db, host, user, password, env, force)

File contrib/sourceforge2trac.py

+"""
+Import a Sourceforge project's tracker items into a Trac database.
+
+Requires:  Development version of Trac 0.7-pre from http://trac.edgewall.com/
+           ElementTree from effbot.org/zone/element.htm
+           Python 2.3 from http://www.python.org/
+           
+The Sourceforge tracker items can be exported from the 'Backup' page of the
+project admin section.
+
+Copyright 2004, Mark Rowe <mrowe@bluewire.net.nz>
+"""
+
+from elementtree.ElementTree import ElementTree
+from datetime import datetime
+import trac.Environment
+
+class FieldParser(object):
+    def __init__(self, e):
+        for field in e:
+            if field.get('name').endswith('date'):
+                setattr(self, field.get('name'), datetime.fromtimestamp(int(field.text)))
+            else:
+                setattr(self, field.get('name'), field.text)        
+
+class ArtifactHistoryItem(FieldParser):
+    def __repr__(self):
+        return '<ArtifactHistoryItem field_name=%r old_value=%r entrydate=%r mod_by=%r>' % (
+            self.field_name, self.old_value, self.entrydate, self.mod_by)
+
+class ArtifactMessage(FieldParser):
+    def __repr__(self):
+        return '<ArtifactMessage adddate=%r user_name=%r body=%r>' % (self.adddate, self.user_name, self.body)
+
+class Artifact(object):
+    def __init__(self, e):
+        self._history = []
+        self._messages = []
+        
+        for field in e:
+            if field.get('name') == 'artifact_history':
+                for h in field:
+                    self._history.append(ArtifactHistoryItem(h))
+            elif field.get('name') == 'artifact_messages':
+                for m in field:
+                    self._messages.append(ArtifactMessage(m))
+            else:
+                setattr(self, field.get('name'), field.text)
+    
+    def history(self):
+        """Returns the history items in reverse chronological order so that the "new value"
+           can easily be calculated based on the final value of the field, and the old value
+           of the items occuring before it.
+        """
+        history = [(h.entrydate, h) for h in self._history]
+        history.sort()
+        return [h[1] for h in history][::-1]
+    
+    def messages(self):
+        return self._messages[:]
+    
+    def __repr__(self):
+        return '<Artifact summary=%r artifact_type=%r category=%r status=%r>' % (self.summary, self.artifact_type, self.category, self.status)
+
+class ExportedProjectData(object):
+    def __init__(self, f):
+        self._artifacts = []
+        
+        root = ElementTree().parse(f)   
+        
+        for artifact in root.find('artifacts'):
+            self._artifacts.append(Artifact(artifact))
+    
+    def artifacts(self):
+        """Returns the artifacts in chronological order, so that they will be assigned numbers in sequence."""
+        artifacts = [(a.open_date, a) for a in self._artifacts]
+        artifacts.sort()
+        return [a[1] for a in artifacts]
+    
+    def featureRequests(self):
+        return [a for a in self._artifacts if a.artifact_type == 'Feature Requests']
+    
+    def bugs(self):
+        return [a for a in self._artifacts if a.artifact_type == 'Bugs']
+    
+    def categories(self):
+        """Returns all the category names that are used, in alphabetical order."""
+        c = {}
+        for a in self._artifacts:
+            c[a.category] = 1
+        
+        categories = c.keys()
+        categories.sort()
+        return categories
+    
+    def groups(self):
+        """Returns all the group names that are used, in alphabetical order."""
+        g = {}
+        for a in self._artifacts:
+            g[a.artifact_group_id] = 1
+        del g['None']
+        
+        groups = g.keys()
+        groups.sort()
+        return groups
+    
+    def artifactTypes(self):
+        """Returns all the artifact types that are used, in alphabetical order."""
+        t = {}
+        for a in self._artifacts:
+            t[a.artifact_type] = 1
+        types = t.keys()
+        types.sort()
+        return types
+
+class TracDatabase(object):
+    def __init__(self, path):
+        self.env = trac.Environment.Environment(path)
+        self._db = self.env.get_db_cnx()
+        self._db.autocommit = False
+    
+    def db(self):
+        return self._db
+    
+    def hasTickets(self):
+        c = self.db().cursor()
+        c.execute('''SELECT count(*) FROM Ticket''')
+        return int(c.fetchall()[0][0]) > 0
+    
+    def setSeverityList(self, s):
+        """Remove all severities, set them to `s`"""
+        if self.hasTickets():
+            raise Exception("Will not modify database with existing tickets!")
+        
+        c = self.db().cursor()
+        c.execute("""DELETE FROM enum WHERE type='severity'""")
+        for i, value in enumerate(s):
+            c.execute("""INSERT INTO enum (type, name, value) VALUES (%s, %s, %s)""",
+                      "severity",
+                      value,
+                      i)
+        self.db().commit()
+    
+    def setPriorityList(self, s):
+        """Remove all priorities, set them to `s`"""
+        if self.hasTickets():
+            raise Exception("Will not modify database with existing tickets!")
+        
+        c = self.db().cursor()
+        c.execute("""DELETE FROM enum WHERE type='priority'""")
+        for i, value in enumerate(s):
+            c.execute("""INSERT INTO enum (type, name, value) VALUES (%s, %s, %s)""",
+                      "priority",
+                      value,
+                      i)
+        self.db().commit()
+
+    
+    def setComponentList(self, l):
+        """Remove all components, set them to `l`"""
+        if self.hasTickets():
+            raise Exception("Will not modify database with existing tickets!")
+        
+        c = self.db().cursor()
+        c.execute("""DELETE FROM component""")
+        for value in l:
+            c.execute("""INSERT INTO component (name) VALUES (%s)""",
+                      value)
+        self.db().commit()
+    
+    def setVersionList(self, v):
+        """Remove all versions, set them to `v`"""
+        if self.hasTickets():
+            raise Exception("Will not modify database with existing tickets!")
+        
+        c = self.db().cursor()
+        c.execute("""DELETE FROM version""")
+        for value in v:
+            c.execute("""INSERT INTO version (name) VALUES (%s)""",
+                      value)
+        self.db().commit()
+        
+    def setMilestoneList(self, m):
+        """Remove all milestones, set them to `m`"""
+        if self.hasTickets():
+            raise Exception("Will not modify database with existing tickets!")
+        
+        c = self.db().cursor()
+        c.execute("""DELETE FROM milestone""")
+        for value in m:
+            c.execute("""INSERT INTO milestone (name) VALUES (%s)""",
+                      value)
+        self.db().commit()
+    
+    def addTicket(self, time, changetime, component,
+                  severity, priority, owner, reporter, cc,
+                  version, milestone, status, resolution,
+                  summary, description, keywords):
+        c = self.db().cursor()
+        if status.lower() == 'open':
+            if owner != '':
+                status = 'assigned'
+            else:
+                status = 'new'
+
+        c.execute("""INSERT INTO ticket (time, changetime, component,
+                                         severity, priority, owner, reporter, cc,
+                                         version, milestone, status, resolution,
+                                         summary, description, keywords)
+                                 VALUES (%s, %s, %s,
+                                         %s, %s, %s, %s, %s,
+                                         %s, %s, %s, %s,
+                                         %s, %s, %s)""",
+                  time, changetime, component,
+                  severity, priority, owner, reporter, cc,
+                  version, milestone, status.lower(), resolution,
+                  summary, '{{{\n%s\n}}}' % (description, ), keywords)
+        self.db().commit()
+        return self.db().db.sqlite_last_insert_rowid()
+    
+    def addTicketComment(self, ticket, time, author, value):
+        c = self.db().cursor()
+        c.execute("""INSERT INTO ticket_change (ticket, time, author, field, oldvalue, newvalue)
+                                 VALUES        (%s, %s, %s, %s, %s, %s)""",
+                  ticket, time.strftime('%s'), author, 'comment', '', '{{{\n%s\n}}}' % (value, ))
+        self.db().commit()
+
+    def addTicketChange(self, ticket, time, author, field, oldvalue, newvalue):
+        c = self.db().cursor()
+        c.execute("""INSERT INTO ticket_change (ticket, time, author, field, oldvalue, newvalue)
+                                 VALUES        (%s, %s, %s, %s, %s, %s)""",
+                  ticket, time.strftime('%s'), author, field, oldvalue, newvalue)
+        self.db().commit()
+
+
+def main():
+    import optparse
+    p = optparse.OptionParser('usage: %prog xml_export.xml /path/to/trac/environment')
+    opt, args = p.parse_args()
+    if len(args) != 2:
+        p.error("Incorrect number of arguments")
+    
+    try:
+        importData(open(args[0]), args[1])
+    except Exception, e:
+        print 'Error:', e
+
+def importData(f, env):
+    project = ExportedProjectData(f)
+    
+    db = TracDatabase(env)
+    db.setSeverityList(project.artifactTypes())
+    db.setComponentList(project.categories())
+    db.setPriorityList(range(1, 11))
+    db.setVersionList(project.groups())
+    db.setMilestoneList([])
+    
+    for a in project.artifacts():
+        i = db.addTicket(time=a.open_date,
+                         changetime='',
+                         component=a.category,
+                         severity=a.artifact_type,
+                         priority=a.priority,
+                         owner=a.assigned_to,
+                         reporter=a.submitted_by,
+                         cc='',
+                         version=a.artifact_group_id,
+                         milestone='',
+                         status=a.status,
+                         resolution=a.resolution,
+                         summary=a.summary,
+                         description=a.details,
+                         keywords='')
+        print 'Imported %s as #%d' % (a.artifact_id, i)
+        for msg in a.messages():
+            db.addTicketComment(ticket=i,
+                                time=msg.adddate,
+                                author=msg.user_name,
+                                value=msg.body)
+        if a.messages():
+            print '    imported %d messages for #%d' % (len(a.messages()), i)
+        
+        values = a.__dict__.copy()
+        field_map = {'summary': 'summary'}
+        for h in a.history():
+            if h.field_name == 'close_date' and values.get(h.field_name, '') == '':
+                f = 'status'
+                oldvalue = 'assigned'
+                newvalue = 'closed'
+            else:
+                f = field_map.get(h.field_name, None)
+                oldvalue = h.old_value
+                newvalue = values.get(h.field_name, '')
+                
+            if f:
+                db.addTicketChange(ticket=i,
+                                   time=h.entrydate,
+                                   author=h.mod_by,
+                                   field=f,
+                                   oldvalue=oldvalue,
+                                   newvalue=newvalue)
+            values[h.field_name] = h.old_value
+
+if __name__ == '__main__':
+    main()