39
|
1 """
|
|
2 Import a Sourceforge project's tracker items into a Trac database.
|
|
3
|
|
4 Requires: Development version of Trac 0.7-pre from http://trac.edgewall.com/
|
|
5 ElementTree from effbot.org/zone/element.htm
|
|
6 Python 2.3 from http://www.python.org/
|
|
7
|
|
8 The Sourceforge tracker items can be exported from the 'Backup' page of the
|
|
9 project admin section.
|
|
10
|
|
11 Copyright 2004, Mark Rowe <mrowe@bluewire.net.nz>
|
|
12 """
|
|
13
|
|
14 from elementtree.ElementTree import ElementTree
|
|
15 from datetime import datetime
|
|
16 import trac.env
|
|
17
|
|
18 class FieldParser(object):
|
|
19 def __init__(self, e):
|
|
20 for field in e:
|
|
21 if field.get('name').endswith('date'):
|
|
22 setattr(self, field.get('name'), datetime.fromtimestamp(int(field.text)))
|
|
23 else:
|
|
24 setattr(self, field.get('name'), field.text)
|
|
25
|
|
26 class ArtifactHistoryItem(FieldParser):
|
|
27 def __repr__(self):
|
|
28 return '<ArtifactHistoryItem field_name=%r old_value=%r entrydate=%r mod_by=%r>' % (
|
|
29 self.field_name, self.old_value, self.entrydate, self.mod_by)
|
|
30
|
|
31 class ArtifactMessage(FieldParser):
|
|
32 def __repr__(self):
|
|
33 return '<ArtifactMessage adddate=%r user_name=%r body=%r>' % (self.adddate, self.user_name, self.body)
|
|
34
|
|
35 class Artifact(object):
|
|
36 def __init__(self, e):
|
|
37 self._history = []
|
|
38 self._messages = []
|
|
39
|
|
40 for field in e:
|
|
41 if field.get('name') == 'artifact_history':
|
|
42 for h in field:
|
|
43 self._history.append(ArtifactHistoryItem(h))
|
|
44 elif field.get('name') == 'artifact_messages':
|
|
45 for m in field:
|
|
46 self._messages.append(ArtifactMessage(m))
|
|
47 else:
|
|
48 setattr(self, field.get('name'), field.text)
|
|
49
|
|
50 def history(self):
|
|
51 """Returns the history items in reverse chronological order so that the "new value"
|
|
52 can easily be calculated based on the final value of the field, and the old value
|
|
53 of the items occuring before it.
|
|
54 """
|
|
55 history = [(h.entrydate, h) for h in self._history]
|
|
56 history.sort()
|
|
57 return [h[1] for h in history][::-1]
|
|
58
|
|
59 def messages(self):
|
|
60 return self._messages[:]
|
|
61
|
|
62 def __repr__(self):
|
|
63 return '<Artifact summary=%r artifact_type=%r category=%r status=%r>' % (self.summary, self.artifact_type, self.category, self.status)
|
|
64
|
|
65 class ExportedProjectData(object):
|
|
66 def __init__(self, f):
|
|
67 self._artifacts = []
|
|
68
|
|
69 root = ElementTree().parse(f)
|
|
70
|
|
71 for artifact in root.find('artifacts'):
|
|
72 self._artifacts.append(Artifact(artifact))
|
|
73
|
|
74 def artifacts(self):
|
|
75 """Returns the artifacts in chronological order, so that they will be assigned numbers in sequence."""
|
|
76 artifacts = [(a.open_date, a) for a in self._artifacts]
|
|
77 artifacts.sort()
|
|
78 return [a[1] for a in artifacts]
|
|
79
|
|
80 def featureRequests(self):
|
|
81 return [a for a in self._artifacts if a.artifact_type == 'Feature Requests']
|
|
82
|
|
83 def bugs(self):
|
|
84 return [a for a in self._artifacts if a.artifact_type == 'Bugs']
|
|
85
|
|
86 def categories(self):
|
|
87 """Returns all the category names that are used, in alphabetical order."""
|
|
88 c = {}
|
|
89 for a in self._artifacts:
|
|
90 c[a.category] = 1
|
|
91
|
|
92 categories = c.keys()
|
|
93 categories.sort()
|
|
94 return categories
|
|
95
|
|
96 def groups(self):
|
|
97 """Returns all the group names that are used, in alphabetical order."""
|
|
98 g = {}
|
|
99 for a in self._artifacts:
|
|
100 g[a.artifact_group_id] = 1
|
|
101 del g['None']
|
|
102
|
|
103 groups = g.keys()
|
|
104 groups.sort()
|
|
105 return groups
|
|
106
|
|
107 def artifactTypes(self):
|
|
108 """Returns all the artifact types that are used, in alphabetical order."""
|
|
109 t = {}
|
|
110 for a in self._artifacts:
|
|
111 t[a.artifact_type] = 1
|
|
112 types = t.keys()
|
|
113 types.sort()
|
|
114 return types
|
|
115
|
|
116 class TracDatabase(object):
|
|
117 def __init__(self, path):
|
|
118 self.env = trac.env.Environment(path)
|
|
119 self._db = self.env.get_db_cnx()
|
|
120 self._db.autocommit = False
|
|
121
|
|
122 def db(self):
|
|
123 return self._db
|
|
124
|
|
125 def hasTickets(self):
|
|
126 c = self.db().cursor()
|
|
127 c.execute('''SELECT count(*) FROM Ticket''')
|
|
128 return int(c.fetchall()[0][0]) > 0
|
|
129
|
|
130 def setTypeList(self, s):
|
|
131 """Remove all types, set them to `s`"""
|
|
132 if self.hasTickets():
|
|
133 raise Exception("Will not modify database with existing tickets!")
|
|
134
|
|
135 c = self.db().cursor()
|
|
136 c.execute("""DELETE FROM enum WHERE kind='ticket_type'""")
|
|
137 for i, value in enumerate(s):
|
|
138 c.execute("""INSERT INTO enum (kind, name, value) VALUES (%s, %s, %s)""",
|
|
139 "ticket_type",
|
|
140 value,
|
|
141 i)
|
|
142 self.db().commit()
|
|
143
|
|
144 def setPriorityList(self, s):
|
|
145 """Remove all priorities, set them to `s`"""
|
|
146 if self.hasTickets():
|
|
147 raise Exception("Will not modify database with existing tickets!")
|
|
148
|
|
149 c = self.db().cursor()
|
|
150 c.execute("""DELETE FROM enum WHERE kind='priority'""")
|
|
151 for i, value in enumerate(s):
|
|
152 c.execute("""INSERT INTO enum (kind, name, value) VALUES (%s, %s, %s)""",
|
|
153 "priority",
|
|
154 value,
|
|
155 i)
|
|
156 self.db().commit()
|
|
157
|
|
158
|
|
159 def setComponentList(self, l):
|
|
160 """Remove all components, set them to `l`"""
|
|
161 if self.hasTickets():
|
|
162 raise Exception("Will not modify database with existing tickets!")
|
|
163
|
|
164 c = self.db().cursor()
|
|
165 c.execute("""DELETE FROM component""")
|
|
166 for value in l:
|
|
167 c.execute("""INSERT INTO component (name) VALUES (%s)""",
|
|
168 value)
|
|
169 self.db().commit()
|
|
170
|
|
171 def setVersionList(self, v):
|
|
172 """Remove all versions, set them to `v`"""
|
|
173 if self.hasTickets():
|
|
174 raise Exception("Will not modify database with existing tickets!")
|
|
175
|
|
176 c = self.db().cursor()
|
|
177 c.execute("""DELETE FROM version""")
|
|
178 for value in v:
|
|
179 c.execute("""INSERT INTO version (name) VALUES (%s)""",
|
|
180 value)
|
|
181 self.db().commit()
|
|
182
|
|
183 def setMilestoneList(self, m):
|
|
184 """Remove all milestones, set them to `m`"""
|
|
185 if self.hasTickets():
|
|
186 raise Exception("Will not modify database with existing tickets!")
|
|
187
|
|
188 c = self.db().cursor()
|
|
189 c.execute("""DELETE FROM milestone""")
|
|
190 for value in m:
|
|
191 c.execute("""INSERT INTO milestone (name) VALUES (%s)""",
|
|
192 value)
|
|
193 self.db().commit()
|
|
194
|
|
195 def addTicket(self, type, time, changetime, component,
|
|
196 priority, owner, reporter, cc,
|
|
197 version, milestone, status, resolution,
|
|
198 summary, description, keywords):
|
|
199 c = self.db().cursor()
|
|
200 if status.lower() == 'open':
|
|
201 if owner != '':
|
|
202 status = 'assigned'
|
|
203 else:
|
|
204 status = 'new'
|
|
205
|
|
206 c.execute("""INSERT INTO ticket (type, time, changetime, component,
|
|
207 priority, owner, reporter, cc,
|
|
208 version, milestone, status, resolution,
|
|
209 summary, description, keywords)
|
|
210 VALUES (%s, %s, %s,
|
|
211 %s, %s, %s, %s, %s,
|
|
212 %s, %s, %s, %s,
|
|
213 %s, %s, %s)""",
|
|
214 type, time, changetime, component,
|
|
215 priority, owner, reporter, cc,
|
|
216 version, milestone, status.lower(), resolution,
|
|
217 summary, '{{{\n%s\n}}}' % (description, ), keywords)
|
|
218 self.db().commit()
|
|
219 return self.db().db.sqlite_last_insert_rowid()
|
|
220
|
|
221 def addTicketComment(self, ticket, time, author, value):
|
|
222 c = self.db().cursor()
|
|
223 c.execute("""INSERT INTO ticket_change (ticket, time, author, field, oldvalue, newvalue)
|
|
224 VALUES (%s, %s, %s, %s, %s, %s)""",
|
|
225 ticket, time.strftime('%s'), author, 'comment', '', '{{{\n%s\n}}}' % (value, ))
|
|
226 self.db().commit()
|
|
227
|
|
228 def addTicketChange(self, ticket, time, author, field, oldvalue, newvalue):
|
|
229 c = self.db().cursor()
|
|
230 c.execute("""INSERT INTO ticket_change (ticket, time, author, field, oldvalue, newvalue)
|
|
231 VALUES (%s, %s, %s, %s, %s, %s)""",
|
|
232 ticket, time.strftime('%s'), author, field, oldvalue, newvalue)
|
|
233 self.db().commit()
|
|
234
|
|
235
|
|
236 def main():
|
|
237 import optparse
|
|
238 p = optparse.OptionParser('usage: %prog xml_export.xml /path/to/trac/environment')
|
|
239 opt, args = p.parse_args()
|
|
240 if len(args) != 2:
|
|
241 p.error("Incorrect number of arguments")
|
|
242
|
|
243 try:
|
|
244 importData(open(args[0]), args[1])
|
|
245 except Exception, e:
|
|
246 print 'Error:', e
|
|
247
|
|
248 def importData(f, env):
|
|
249 project = ExportedProjectData(f)
|
|
250
|
|
251 db = TracDatabase(env)
|
|
252 db.setTypeList(project.artifactTypes())
|
|
253 db.setComponentList(project.categories())
|
|
254 db.setPriorityList(range(1, 11))
|
|
255 db.setVersionList(project.groups())
|
|
256 db.setMilestoneList([])
|
|
257
|
|
258 for a in project.artifacts():
|
|
259 i = db.addTicket(type=a.artifact_type,
|
|
260 time=a.open_date,
|
|
261 changetime='',
|
|
262 component=a.category,
|
|
263 priority=a.priority,
|
|
264 owner=a.assigned_to,
|
|
265 reporter=a.submitted_by,
|
|
266 cc='',
|
|
267 version=a.artifact_group_id,
|
|
268 milestone='',
|
|
269 status=a.status,
|
|
270 resolution=a.resolution,
|
|
271 summary=a.summary,
|
|
272 description=a.details,
|
|
273 keywords='')
|
|
274 print 'Imported %s as #%d' % (a.artifact_id, i)
|
|
275 for msg in a.messages():
|
|
276 db.addTicketComment(ticket=i,
|
|
277 time=msg.adddate,
|
|
278 author=msg.user_name,
|
|
279 value=msg.body)
|
|
280 if a.messages():
|
|
281 print ' imported %d messages for #%d' % (len(a.messages()), i)
|
|
282
|
|
283 values = a.__dict__.copy()
|
|
284 field_map = {'summary': 'summary'}
|
|
285 for h in a.history():
|
|
286 if h.field_name == 'close_date' and values.get(h.field_name, '') == '':
|
|
287 f = 'status'
|
|
288 oldvalue = 'assigned'
|
|
289 newvalue = 'closed'
|
|
290 else:
|
|
291 f = field_map.get(h.field_name, None)
|
|
292 oldvalue = h.old_value
|
|
293 newvalue = values.get(h.field_name, '')
|
|
294
|
|
295 if f:
|
|
296 db.addTicketChange(ticket=i,
|
|
297 time=h.entrydate,
|
|
298 author=h.mod_by,
|
|
299 field=f,
|
|
300 oldvalue=oldvalue,
|
|
301 newvalue=newvalue)
|
|
302 values[h.field_name] = h.old_value
|
|
303
|
|
304 if __name__ == '__main__':
|
|
305 main()
|