18
18
# 2. Create a separate repo for testing purposes. THERE IS NO UNDO!
19
19
# 3. Copy bugzilla2github.conf.sample to bugzilla2github.conf
20
20
# - Change all settings to fit your setup
21
- # 4. Run the script. Good luck....
21
+ # 4. Please understand there is not API to delete issues. So test thoroughly before the final run!
22
+ # 5. Run the script. Good luck....
22
23
23
- import json , getopt , os , pprint , re , requests , sys , time , xml . etree . ElementTree
24
+ import json , requests , sys
24
25
import psycopg2 , psycopg2 .extras
25
26
import ConfigParser
26
- from pprint import pprint ,pformat
27
- from urlparse import urljoin
27
+ from pprint import pprint
28
+ from dateutil .tz import tzlocal
29
+
28
30
29
31
reload (sys )
30
32
sys .setdefaultencoding ('utf-8' )
@@ -68,7 +70,8 @@ def read_bugs(conn):
68
70
# create a new cursor object
69
71
cur = conn .cursor (cursor_factory = psycopg2 .extras .DictCursor )
70
72
# execute the SELECT statement
71
- cur .execute (""" SELECT * from bugs where bug_id=1295 """ ,)
73
+ #cur.execute(""" SELECT * from bugs where bug_id=1284 """,)
74
+ cur .execute (""" SELECT * from bugs order by bug_id """ , )
72
75
colnames = [desc [0 ] for desc in cur .description ]
73
76
results = cur .fetchall ()
74
77
except (Exception , psycopg2 .DatabaseError ) as error :
@@ -174,8 +177,69 @@ def get_reporter(reporters,reporter_id):
174
177
reporterstr = reporterobj ["realname" ]+ " \<<" + reporterobj ["login_name" ]+ ">\>"
175
178
return reporterstr
176
179
180
+ def get_components (conn ):
181
+ """
182
+ Retrieve all components from Bugzilla database
183
+ :param conn: database connection object
184
+ :return: object with all results
185
+ """
186
+ try :
187
+ # create a new cursor object
188
+ cur = conn .cursor (cursor_factory = psycopg2 .extras .DictCursor )
189
+ # execute the SELECT statement
190
+ cur .execute (""" SELECT * FROM components """ )
191
+ results = cur .fetchall ()
192
+ colnames = [desc [0 ] for desc in cur .description ]
193
+ except (Exception , psycopg2 .DatabaseError ) as error :
194
+ print (error )
195
+ # Build indexed dictionary from results
196
+ components = {}
197
+ for result in results :
198
+ components [result ["id" ]]= result
199
+ return components ,colnames
200
+
201
+
202
+ def get_component (components ,component_id ):
203
+ """
204
+ routine to find componentname for componentid
205
+ :param components: dictionary with all components, indexed on componentid
206
+ :param component_id: id of component to find
207
+ :return: string componentname
208
+ """
209
+ # ['userid', 'login_name', 'cryptpassword', 'realname', 'disabledtext', 'disable_mail', 'mybugslink', 'extern_id']
210
+ componentobj = components [component_id ]
211
+ componentstr = componentobj ["name" ]
212
+ return componentstr
213
+
214
+ def get_duplicates (conn ):
215
+ """
216
+ Because we have relative few duplicates (55) compared to the total number of bugs (1709)
217
+ we retrieve all duplicates from Bugzilla database into dictionary of lists for quick lookup
218
+ :param conn: database connection object
219
+ :return: object with all results (a dictionary of lists)
220
+ """
221
+ duplicates = dict ()
222
+ try :
223
+ # create a new cursor object
224
+ cur = conn .cursor (cursor_factory = psycopg2 .extras .DictCursor )
225
+ # execute the SELECT statement
226
+ cur .execute (""" SELECT * FROM duplicates """ )
227
+ results = cur .fetchall ()
228
+
229
+ for result in results :
230
+ if duplicates .get (result ["dupe_of" ]) is None :
231
+ duplicates [result ["dupe_of" ]]= list ()
232
+ duplicates [result ["dupe_of" ]].append (result ["dupe" ])
233
+ else :
234
+ duplicates [result ["dupe_of" ]].append (result ["dupe" ])
235
+
236
+ except (Exception , psycopg2 .DatabaseError ) as error :
237
+ print (error )
238
+
239
+ return duplicates
240
+
177
241
178
- def create_body (reporters ,bug ):
242
+ def create_body (reporters ,duplicates , bug ):
179
243
"""
180
244
Routine to create issue body text
181
245
:param bug: object containing bug record
@@ -191,6 +255,37 @@ def create_body(reporters,bug):
191
255
192
256
#
193
257
# Last updated: 2012-09-10 05:56:17 +0000
258
+ #
259
+ # Example record info
260
+ # bug_id -> 1295
261
+ # assigned_to -> 6
262
+ # bug_file_loc ->
263
+ # bug_severity -> critical
264
+ # bug_status -> RESOLVED
265
+ # creation_ts -> 2011-12-14 18:05:08
266
+ # delta_ts -> 2012-01-13 10:31:16
267
+ # short_desc -> TaskSource is null for some TaskElements
268
+ # op_sys -> All
269
+ # priority -> P5
270
+ # product_id -> 2
271
+ # rep_platform -> All
272
+ # reporter -> 6
273
+ # version -> libreplan-1.2 (1.2.x)
274
+ # component_id -> 10
275
+ # resolution -> FIXED
276
+ # target_milestone -> ---
277
+ # qa_contact -> None
278
+ # status_whiteboard ->
279
+ # votes -> 0
280
+ # lastdiffed -> 2012-01-13 10:31:16
281
+ # everconfirmed -> 1
282
+ # reporter_accessible -> 1
283
+ # cclist_accessible -> 1
284
+ # estimated_time -> 0.00
285
+ # remaining_time -> 0.00
286
+ # deadline -> None
287
+ # alias -> None
288
+ # cf_browser -> ---
194
289
body = ""
195
290
196
291
# ret["body"].append("# Bugzilla Bug ID: %d" % ret["number"])
@@ -212,19 +307,18 @@ def create_body(reporters,bug):
212
307
# ret["body"].append("Version: " + ret["version"])
213
308
body += "Version: " + str (bug ["version" ])+ "\n "
214
309
215
- # [I'm an inline-style link](https://www.google.com)
216
- body += "\n ---\n "
217
- body += "(Note: this issue was migrated automatically with [bugzilla2github.py tool](https://github.com/LibrePlan/bugzilla2github) )\n "
218
-
219
-
220
310
# if "cc" in bug:
221
311
# ret["body"].append("CC: " + ", ".join(emails_convert(bug.pop("cc"))))
222
312
# TODO: chose not to support this. Use Link to original issue to find info
313
+
223
314
# # Extra information
224
315
# ret["body"].append("")
225
316
# if "dup_id" in bug:
226
317
# ret["body"].append("Duplicates: " + ids_convert(bug.pop("dup_id")))
227
- # TODO: chose not to support this. Use Link to original issue to find info
318
+ if duplicates .get (bug ["bug_id" ]) is not None :
319
+ for duplicate in duplicates [bug ["bug_id" ]]:
320
+ body += "Duplicate: [" + str (duplicate ) + "](" + BUGZILLA_URL + "show_bug.cgi?id=" + str (bug ["bug_id" ])+ "))\n "
321
+
228
322
# if "dependson" in bug:
229
323
# ret["body"].append("Depends on: " + ids_convert(bug.pop("dependson")))
230
324
# TODO: chose not to support this. Use Link to original issue to find info
@@ -233,9 +327,14 @@ def create_body(reporters,bug):
233
327
# TODO: chose not to support this. Use Link to original issue to find info
234
328
# if "see_also" in bug:
235
329
# ret["body"].append("See also: " + bug.pop("see_also"))
236
- # TODO: chose not to support this. Use Link to original issue to find info
330
+
237
331
# ret["body"].append("Last updated: " + ret["updated_at"])
332
+ body += "Last updated: " + str (bug ["delta_ts" ]) + "\n "
238
333
# ret["body"].append("")
334
+ # [I'm an inline-style link](https://www.google.com)
335
+ body += "\n ---\n "
336
+ body += "(Note: this issue was migrated automatically with [bugzilla2github.py tool](https://github.com/LibrePlan/bugzilla2github) )\n "
337
+
239
338
return body
240
339
241
340
# Start connection to database
@@ -247,26 +346,31 @@ def create_body(reporters,bug):
247
346
248
347
# read all bug reporters
249
348
reporters ,colnames = get_reporters (conn )
349
+
350
+ # read all components
351
+ components ,colnames = get_components (conn )
352
+
353
+ # Create a duplicates lookup dictionary of lists
354
+ duplicates = get_duplicates (conn )
355
+
250
356
# read all bug reports
251
357
bugs ,colnames = read_bugs (conn )
358
+
252
359
#pprint(bugs)
253
360
#pprint(colnames)
254
361
issue = {}
255
362
for bug in bugs :
256
363
bug_id = bug ['bug_id' ]
257
- for field in colnames :
258
- print str (field ) + " -> " + str (bug [str (field )])
364
+ # for field in colnames:
365
+ # print str(field) + " -> " + str(bug[str(field)])
259
366
print "bug_id" ,bug_id
260
- #print "bug_severity",bug['bug_severity']
261
- #print "bug_status", bug['bug_status']
262
- #print "short_desc", bug['short_desc']
263
- #print "priority",bug['priority']
264
- #print "version",bug['version']
265
- #print "resolution",bug['resolution']
266
367
267
- body = create_body (reporters ,bug )
368
+ # Check for duplicates
369
+ #duplicates = find_duplicates(conn,bug_id)
268
370
269
- # Create issues object to send to GitHub
371
+ body = create_body (reporters ,duplicates ,bug )
372
+
373
+ # Create issues object to send to GitHub: only title and body are needed.
270
374
# And an issue with a comment only needs the comment body added:
271
375
#
272
376
# {
@@ -281,17 +385,62 @@ def create_body(reporters,bug):
281
385
# ]
282
386
# }
283
387
388
+ # Complete info:
389
+ # "title": "Imported from some other system",
390
+ # "body": "...",
391
+ # "created_at": "2014-01-01T12:34:58Z",
392
+ # "closed_at": "2014-01-02T12:24:56Z",
393
+ # "updated_at": "2014-01-03T11:34:53Z",
394
+ # "assignee": "jonmagic",
395
+ # "milestone": 1,
396
+ # "closed": true,
397
+ # "labels": [
398
+ # "bug",
399
+ # "low"
400
+ # ]
401
+
402
+ if bug ["bug_status" ]== "RESOLVED" :
403
+ closed = True
404
+ else :
405
+ closed = False
406
+
407
+ # Let's create some labels
408
+ # NOTE to self: An empty label means issue not added to GitHub!
409
+ labels = []
410
+ if bug ["bug_severity" ]:
411
+ labels .append (bug ["bug_severity" ])
412
+ if bug ["priority" ]:
413
+ labels .append (bug ["priority" ])
414
+ if bug ["resolution" ]:
415
+ labels .append (bug ["resolution" ].lower ())
416
+ bug_component = get_component (components ,bug ["component_id" ])
417
+ if bug_component :
418
+ labels .append (bug_component )
419
+ #labels.append("mytest")
420
+
421
+ # TODO We currently do not know WHEN an issue was closed, so do not store it in the issue.
422
+ # Well, we could find it through bug_activity table but too much hassle actually.
423
+ # Good news is, that we do close an issue when it is closed
424
+ # "closed_at":str(bug["delta_ts"].replace(tzinfo=tzlocal()).isoformat()),
284
425
issue = {"issue" : {"title" :bug ['short_desc' ],
285
- "body" :body }}
426
+ "body" :body ,
427
+ "created_at" : str (bug ["creation_ts" ].replace (tzinfo = tzlocal ()).isoformat ()),
428
+ "closed" : closed ,
429
+ "labels" : labels ,
430
+ }
431
+ }
432
+
433
+ #pprint(issue)
286
434
287
435
issuecomments = []
288
436
pprint (issuecomments )
289
437
comments ,colnames = read_comments (conn ,bug_id )
290
- pprint (colnames )
291
438
for comment in comments :
292
439
# ['comment_id', 'bug_id', 'who', 'bug_when', 'work_time', 'thetext', 'isprivate', 'already_wrapped', 'type', 'extra_data']
293
440
comment_id = comment ["comment_id" ]
294
441
attach_id = comment ["extra_data" ]
442
+ comment_type = comment ["type" ] # Very important: 5=attachment
443
+
295
444
issuecomment = ""
296
445
# pprint(comment)
297
446
print " comment_id" , comment ["comment_id" ]
@@ -302,14 +451,13 @@ def create_body(reporters,bug):
302
451
# ret.append("Date: " + comment.pop("bug_when"))
303
452
issuecomment += "Date: " + str (comment ["bug_when" ]) + "\n "
304
453
# ret.append("From: " + email_convert(comment.pop("who"),
305
- issuecomment += "From: " + get_reporter (reporters , comment ["who" ]) + "\n "
454
+ issuecomment += "From: " + get_reporter (reporters , comment ["who" ]) + "\n \n "
306
455
# comment.pop("who.name", None)))
307
- # ret.append("")
308
456
# ret.append(comment.pop("thetext", ""))
309
457
issuecomment += comment ["thetext" ] + "\n \n "
310
458
311
459
312
- if attach_id is not None :
460
+ if comment_type == 5 and attach_id is not None :
313
461
print " attach_id" ,attach_id
314
462
# check attachment record for this comment
315
463
attachment ,colnames = read_attachment (conn ,attach_id )
@@ -318,7 +466,6 @@ def create_body(reporters,bug):
318
466
# 'attach_id', 'bug_id', 'creation_ts', 'modification_time', 'description', 'mimetype', 'ispatch', 'filename',
319
467
# 'submitter_id', 'isobsolete', 'isprivate', 'isurl']
320
468
#pprint(attachment)
321
- #attachment=attachment.pop
322
469
filename = attachment ["filename" ]
323
470
#filesize=attachment[""]
324
471
print " We have an attachment:" ,filename
@@ -327,41 +474,52 @@ def create_body(reporters,bug):
327
474
# attach.pop("type"), attach.pop("size")))
328
475
# ret.append("> Description: " + attach.pop("desc"))
329
476
# TODO: create link to repo file as: https://github.com/LibrePlan/bugzilla2githubattachments/blob/master/LICENSE
330
- issuecomment += "\n ---\n \ n Attached file: [" + filename + "](" + FILEREPO_URL + str (bug_id ) + '_' + str (comment_id ) + "_" + filename + ")\n "
331
- issuecomment += "Description : " + attachment ["description" ] + "\n --- \n "
477
+ issuecomment += "\n ---\n Attached file: [" + filename + "](" + FILEREPO_URL + str (bug_id ) + '_' + str (comment_id ) + "_" + filename + ")\n "
478
+ issuecomment += "File description : " + attachment ["description" ] + "\n "
332
479
333
480
# add stuff to issuecomments list
334
481
issuedict = dict ()
335
482
issuedict ["body" ] = issuecomment
336
483
issuecomments .append (issuedict )
337
484
338
-
339
- #issue["issue"].append(issuecomments)
340
- #commentsdict={ "comments", issuecomments }
341
- #commentsdict=dict()
342
- #commentsdict.setdefault("comments", issuecomments)
343
485
issue ["comments" ]= issuecomments
344
486
# What have we created?
487
+ print "*" * 40 + " Final issue object " + "*" * 40
345
488
pprint (issue )
346
489
347
490
# Now let's try to add this issue to a github repo
348
491
# https://api.github.com/repos/${GITHUB_USERNAME}/foo/import/issues
349
492
urlparts = (str (GITHUB_URL ),"repos" ,str (GITHUB_OWNER ) , str (GITHUB_REPO ) , "import/issues" )
350
493
url = "/" .join (urlparts )
351
- # if url[0] == "/":
352
- # u = "%s%s" % (GITHUB_URL, url)
353
- # else:
354
- # u = "%s/repos/%s/%s/%s" % (GITHUB_URL, GITHUB_OWNER, GITHUB_REPO, url)
494
+
355
495
pprint (url )
356
496
d = issue
357
- #sys.exit(4)
358
- #result = requests.post(u, params={"access_token": GITHUB_TOKEN,
359
497
headers = {"Authorization" : "token " + GITHUB_TOKEN ,
360
498
"Accept" : "application/vnd.github.golden-comet-preview+json" }
361
499
result = requests .post (url , headers = headers , data = json .dumps (d ))
500
+ print result .status_code
501
+ # Check if something went wrong
502
+ if result .status_code <> 202 :
503
+ print result .json ()
504
+ result_dict = result .json ()
505
+ # Let's check if it all worked
506
+ result_id = result_dict ["id" ]
507
+ import_issues_url = result_dict ["import_issues_url" ]
508
+ print "Result ID = " + str (result_id )
509
+ print "import_issues_url= " + str (result_dict ["import_issues_url" ])
510
+ # First get ID of issue added
511
+ # Next request result
512
+ # curl -H "Authorization: token ${GITHUB_TOKEN}" \
513
+ #-H "Accept: application/vnd.github.golden-comet-preview+json" \
514
+ #https://api.github.com/repos/#{GITHUB_USERNAME}/foo/import/issues/7
515
+ url2 = import_issues_url + "/" + str (result_id )
516
+ #print(url2)
517
+ result2 = requests .get (import_issues_url , headers = headers , data = json .dumps (d ))
518
+ pprint (result2 )
519
+ print result2 .json ()
520
+ # Bail out on error
521
+ sys .exit (2 )
362
522
363
- pprint (result )
364
- print result .json ()
365
523
# The end of handling all bugs
366
524
367
525
# close the communication with the PostgresQL database
0 commit comments