#!/usr/bin/env python # Generate JSON to import Trac tickets into GitHub issues using the new import API # described at https://gist.github.com/jonmagic/5282384165e0f86ef105 import os import time import json import yaml import sqlite3 import hashlib import argparse import subprocess ticket_query = ''' SELECT id, type, owner, reporter, milestone, status, resolution, summary, description, component, priority, time / 1000000 AS createdtime, changetime / 1000000 AS modifiedtime FROM ticket ORDER BY id ''' comment_query = ''' SELECT time / 1000000 AS createdtime, author, newvalue FROM ticket_change WHERE ticket = ? AND field = 'comment' AND newvalue <> '' ORDER BY time ''' attachment_query = ''' SELECT id, filename, size, author, description, ipnr, time / 1000000 AS createdtime FROM attachment WHERE id = ? AND type = 'ticket' ORDER BY time, filename ''' def isotime(t): return None if t == 0 else time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(t)) def hashname(whatever): return hashlib.sha1(unicode(whatever)).hexdigest() def ticket_text(ticket): d = dict(ticket, createdtime = isotime(ticket["createdtime"]), modifiedtime = isotime(ticket["modifiedtime"])) return u"{description}\n\n" \ u"_Trac ticket #{id} component {component} priority {priority}, owner {owner}," \ u" created by {reporter} on {createdtime}, last modified {modifiedtime}_\n".format(**d) def comment_text(comment): d = dict(comment, createdtime = isotime(comment["createdtime"])) return u"{newvalue}\n\n_Trac comment by {author} on {createdtime}_\n".format(**d) def attachment_text(attachment): h1 = hashname(attachment["id"]) h2 = hashname(attachment["filename"]) fn2 = os.path.splitext(attachment["filename"])[1] fn = os.path.join(gist_url, h1[:3], h1, h2 + fn2) url = "{}/raw/{}/ticket.{}.{}{}".format(gist_url.rstrip("/"), gist_commit, h1, h2, fn2) d = dict(attachment, createdtime = isotime(comment["createdtime"]), url = url) return u"[{filename}]({url}) {description}\n_Trac attachment by {author} on {createdtime}_\n".format(**d) def comment_merge(comments, attachments): result = [] while comments and attachments: result.append(comments.pop(0) if comments[0]["created_at"] <= attachments[0]["created_at"] else attachments.pop(0)) return result + comments + attachments parser = argparse.ArgumentParser(formatter_class = argparse.ArgumentDefaultsHelpFormatter) parser.add_argument("-c", "--config", type = argparse.FileType(), default = "generate-json.yaml", help = "YAML config mappings") args = parser.parse_args() cfg = yaml.safe_load(args.config) assignee_map = cfg["assignees"] type_map = cfg["type_labels"] resolution_map = cfg["resolution_labels"] gist_url = cfg.get("attachment_gist_url") if gist_url is not None: gist_commit = subprocess.check_output(("git", "ls-remote", gist_url, "HEAD")).split()[0] db = sqlite3.connect(cfg["database"]) db.row_factory = sqlite3.Row ticket_cursor = db.cursor() comment_cursor = db.cursor() attachment_cursor = db.cursor() if not os.path.isdir(cfg["ticket_directory"]): os.makedirs(cfg["ticket_directory"]) for ticket in ticket_cursor.execute(ticket_query): comments = comment_merge([dict(created_at = isotime(comment["createdtime"]), body = comment_text(comment)) for comment in comment_cursor.execute(comment_query, (ticket["id"],))], [] if gist_url is None else [dict(created_at = isotime(attachment["createdtime"]), body = attachment_text(attachment)) for attachment in attachment_cursor.execute(attachment_query, (ticket["id"],))]) issue = dict( title = ticket["summary"], body = ticket_text(ticket), created_at = isotime(ticket["createdtime"]), updated_at = isotime(ticket["modifiedtime"])) if ticket["status"] == "closed": issue["closed"] = True issue["closed_at"] = isotime(ticket["modifiedtime"]) comments.append(dict(created_at = isotime(ticket["modifiedtime"]), body = "_Closed with resolution {resolution}_\n".format(**ticket))) if ticket["owner"] in assignee_map: issue["assignee"] = assignee_map[ticket["owner"]] labels = [type_map.get(ticket["type"]), resolution_map.get(ticket["resolution"])] while None in labels: del labels[labels.index(None)] if labels: issue["labels"] = labels issue = dict(issue = issue) if comments: issue["comments"] = comments with open(os.path.join(cfg["ticket_directory"], "ticket_{:03d}.json".format(ticket["id"])), "wb") as f: json.dump(issue, f, indent = 4, sort_keys = True, separators=(",", ": "))