| 1 | # FogBugz2CSV Migration Tool |
|---|
| 2 | |
|---|
| 3 | import MySQLdb |
|---|
| 4 | import xml.sax.saxutils as saxutils |
|---|
| 5 | |
|---|
| 6 | DB_HOSTNAME = 'localhost' # FogBugz's database hostname (i.e.: 'localhost') |
|---|
| 7 | DB_USERNAME = '' # FogBugz's database username |
|---|
| 8 | DB_PASSWORD = '' # FogBugz's database password |
|---|
| 9 | DB_DATABASE = 'fogbugz' # FogBugz's database |
|---|
| 10 | |
|---|
| 11 | # Using FogBugz's 'category' for conversion |
|---|
| 12 | # In Trac, 'type' must be one of: task, defect, enhancement |
|---|
| 13 | TYPE = { |
|---|
| 14 | '1': 'defect', # Bug in FogBugz, Defect in Trac |
|---|
| 15 | '2': 'enhancement', # Feature in FogBugz, Enhancement in Trac |
|---|
| 16 | '3': 'task', # Inquiry in FogBugz, Task in Trac |
|---|
| 17 | '4': 'task', # Schedule Item in FogBugz, Task in Trac |
|---|
| 18 | } |
|---|
| 19 | |
|---|
| 20 | # FogBugz's 'priority' for conversion |
|---|
| 21 | # In Trac, 'severity' must be one of: blocker, critical, major, normal, minor, trivial, enhancement |
|---|
| 22 | SEVERITY = { |
|---|
| 23 | '1': 'blocker', # Blocker in FogBugz, Blocker in Trac |
|---|
| 24 | '2': 'critical', # Critical in FogBugz, Critical in Trac |
|---|
| 25 | '3': 'major', # Major in FogBugz, Major in Trac |
|---|
| 26 | '4': 'normal', # Normal in FogBugz, Normal in Trac |
|---|
| 27 | '5': 'minor', # Minor in FogBugz, Minor in Trac |
|---|
| 28 | '6': 'trivial', # Trivial in FogBugz, Trivial in Trac |
|---|
| 29 | '7': 'enhancement', # Enhancement in FogBugz, Enhancement in Trac |
|---|
| 30 | } |
|---|
| 31 | |
|---|
| 32 | # Not in FogBugz, using 'priority' for conversion |
|---|
| 33 | # In Trac, 'priority' must be one of: highest, high, normal, low, lowest |
|---|
| 34 | PRIORITY = { |
|---|
| 35 | '1': 'highest', # Blocker in FogBugz, Highest in Trac |
|---|
| 36 | '2': 'high', # Critical in FogBugz, High in Trac |
|---|
| 37 | '3': 'high', # Major in FogBugz, High in Trac |
|---|
| 38 | '4': 'normal', # Normal in FogBugz, Normal in Trac |
|---|
| 39 | '5': 'low', # Minor in FogBugz, Low in Trac |
|---|
| 40 | '6': 'low', # Trivial in FogBugz, Low in Trac |
|---|
| 41 | '7': 'lowest', # Enhancement in FogBugz, Lowest in Trac |
|---|
| 42 | } |
|---|
| 43 | |
|---|
| 44 | # Using Fogbugz's 'status' for conversion |
|---|
| 45 | # In Trac, 'status' must be one of: new, assigned, reopened, closed |
|---|
| 46 | STATUS = { |
|---|
| 47 | '1': 'assigned', # Active in FogBugz, Assigned in Trac |
|---|
| 48 | '2': 'closed', # Resolved (Fixed) in FogBugz, Closed in Trac |
|---|
| 49 | '3': 'closed', # Resolved (Not Reproducible) in FogBugz, Closed in Trac |
|---|
| 50 | '4': 'closed', # Resolved (Duplicate) in FogBugz, Closed in Trac |
|---|
| 51 | '5': 'closed', # Resolved (Postponed) in FogBugz, Closed in Trac |
|---|
| 52 | '6': 'closed', # Resolved (Won't Fix) in FogBugz, Closed in Trac |
|---|
| 53 | '7': 'closed', # Resolved (By Design) in FogBugz, Closed in Trac |
|---|
| 54 | '8': 'closed', # Resolved (Implemented) in FogBugz, Closed in Trac |
|---|
| 55 | '9': 'closed', # Resolved (Won't Impement) in FogBugz, Closed in Trac |
|---|
| 56 | '10': 'closed', # Resolved (Already Exists) in FogBugz, Closed in Trac |
|---|
| 57 | '11': 'closed', # Resolved (Responded) in FogBugz, Closed in Trac |
|---|
| 58 | '12': 'closed', # Resolved (Won't Respond) in FogBugz, Closed in Trac |
|---|
| 59 | '13': 'closed', # Resolved (SPAM) in FogBugz, Closed in Trac |
|---|
| 60 | '14': 'closed', # Resolved (Waiting For Info) in FogBugz, Closed in Trac |
|---|
| 61 | '15': 'closed', # Resolved (Completed) in FogBugz, Closed in Trac |
|---|
| 62 | '16': 'closed', # Resolved (Canceled) in FogBugz, Closed in Trac |
|---|
| 63 | } |
|---|
| 64 | |
|---|
| 65 | # Each line in the CSV file needs to have the following entries |
|---|
| 66 | # [M] type text -- the ticket purpose |
|---|
| 67 | # time integer -- the time it was created |
|---|
| 68 | # changetime integer |
|---|
| 69 | # component text |
|---|
| 70 | # [M] severity text |
|---|
| 71 | # [M] priority text |
|---|
| 72 | # owner text -- who is this ticket assigned to |
|---|
| 73 | # reporter text |
|---|
| 74 | # cc text -- email addresses to notify |
|---|
| 75 | # url text -- url related to this ticket |
|---|
| 76 | # version text -- |
|---|
| 77 | # milestone text -- |
|---|
| 78 | # [M] status text |
|---|
| 79 | # resolution text |
|---|
| 80 | # summary text -- one-line summary |
|---|
| 81 | # description text -- problem description (long) |
|---|
| 82 | # keywords text |
|---|
| 83 | |
|---|
| 84 | def format_value(value): |
|---|
| 85 | string = str(value) |
|---|
| 86 | string = string.replace("\\'", "'") |
|---|
| 87 | string = string.replace("'", "''") |
|---|
| 88 | string = string.replace("\r", "") |
|---|
| 89 | string = string.replace("\n", "\\n") |
|---|
| 90 | string = string.replace('"', '""') |
|---|
| 91 | string = string.replace(' ', '') |
|---|
| 92 | string = string.replace('"', '""') |
|---|
| 93 | return '"' + saxutils.unescape(string) + '"' |
|---|
| 94 | |
|---|
| 95 | def get_username(full_name): |
|---|
| 96 | """ |
|---|
| 97 | Returns Trac's username |
|---|
| 98 | """ |
|---|
| 99 | names = full_name.lower().split(' ') |
|---|
| 100 | if len(names) > 1: |
|---|
| 101 | # First letter from the first name and complete surname |
|---|
| 102 | # i.e.: John Doe will be converted to 'jdoe' |
|---|
| 103 | return names[0][0] + names[1] |
|---|
| 104 | else: |
|---|
| 105 | return full_name |
|---|
| 106 | |
|---|
| 107 | def main(): |
|---|
| 108 | db = MySQLdb.connect( |
|---|
| 109 | host=DB_HOSTNAME, |
|---|
| 110 | user=DB_USERNAME, |
|---|
| 111 | passwd=DB_PASSWORD, |
|---|
| 112 | db=DB_DATABASE |
|---|
| 113 | ) |
|---|
| 114 | cursor = db.cursor() |
|---|
| 115 | # Getting data from FogBugz's database |
|---|
| 116 | sql = """ |
|---|
| 117 | SELECT |
|---|
| 118 | Bug.ixCategory AS type, |
|---|
| 119 | UNIX_TIMESTAMP(Bug.dtOpened) AS time, |
|---|
| 120 | UNIX_TIMESTAMP(NOW()) AS changetime, |
|---|
| 121 | Project.sProject AS component, |
|---|
| 122 | Bug.ixPriority AS severity, |
|---|
| 123 | Bug.ixPriority AS priority, |
|---|
| 124 | Owner.sFullName AS owner, |
|---|
| 125 | Reporter.sFullName AS reporter, |
|---|
| 126 | '' AS cc, |
|---|
| 127 | '' AS url, |
|---|
| 128 | Bug.sVersion AS version, |
|---|
| 129 | FixFor.sFixFor AS milestone, |
|---|
| 130 | Bug.ixStatus as status, |
|---|
| 131 | '' AS resolution, |
|---|
| 132 | Bug.sTitle AS summary, |
|---|
| 133 | (SELECT s FROM BugEvent WHERE BugEvent.ixBug = Bug.ixBug ORDER BY BugEvent.ixBugEvent ASC LIMIT 1) AS description, |
|---|
| 134 | '' AS keywords |
|---|
| 135 | FROM Bug |
|---|
| 136 | LEFT JOIN Project ON Project.ixProject = Bug.ixProject |
|---|
| 137 | LEFT JOIN Person AS Owner ON Owner.ixPerson = Bug.ixPersonAssignedTo |
|---|
| 138 | LEFT JOIN Person AS Reporter ON Reporter.ixPerson = Bug.ixPersonOpenedBy |
|---|
| 139 | LEFT JOIN FixFor ON FixFor.ixFixFor = Bug.ixFixFor |
|---|
| 140 | LEFT JOIN BugEvent ON Bug.ixBugEventLatestText = BugEvent.ixBugEvent |
|---|
| 141 | ORDER BY Bug.ixBug ASC; |
|---|
| 142 | """ |
|---|
| 143 | cursor.execute(sql) |
|---|
| 144 | result = cursor.fetchall() |
|---|
| 145 | # CSV header |
|---|
| 146 | print 'type,time,changetime,component,severity,priority,owner,reporter,cc,url,version,milestone,status,resolution,summary,description,keywords' |
|---|
| 147 | for row in result: |
|---|
| 148 | export = ( |
|---|
| 149 | TYPE.get(str(row[0])), # type, converting FogBugz's 'category' to Trac's 'type' |
|---|
| 150 | str(row[1]), # time |
|---|
| 151 | str(row[2]), # changetime |
|---|
| 152 | str(row[3]), # component |
|---|
| 153 | SEVERITY.get(str(row[4])), # severity, converting FogBugz's 'priority' to Trac's 'severity' |
|---|
| 154 | PRIORITY.get(str(row[5])), # priority, converting FogBugz's 'priority' to Trac's 'priority' |
|---|
| 155 | get_username(str(row[6])), # owner |
|---|
| 156 | get_username(str(row[7])), # reporter |
|---|
| 157 | str(row[8]), # cc |
|---|
| 158 | str(row[9]), # url |
|---|
| 159 | str(row[10]), # version |
|---|
| 160 | str(row[11]), # milestone |
|---|
| 161 | STATUS.get(str(row[12])), # status, converting FogBugz's 'status' to Trac's 'status' |
|---|
| 162 | str(row[13]), # resolution |
|---|
| 163 | format_value(str(row[14])), # summary (escaped) |
|---|
| 164 | format_value(str(row[15])), # description (escaped) |
|---|
| 165 | format_value(str(row[16])), # keywords |
|---|
| 166 | ) |
|---|
| 167 | print ','.join(export) # Don't like csv module, sorry |
|---|
| 168 | |
|---|
| 169 | if __name__ == '__main__': |
|---|
| 170 | main() |
|---|