summaryrefslogtreecommitdiff
path: root/references/generate-json.py
blob: b8b1f389907ab341953486a7903c3d60f152fc9c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
pre { line-height: 125%; }
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.highlight .hll { background-color: #ffffcc }
.highlight .c { color: #888888 } /* Comment */
.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
.highlight .k { color: #008800; font-weight: bold } /* Keyword */
.highlight .ch { color: #888888 } /* Comment.Hashbang */
.highlight .cm { color: #888888 } /* Comment.Multiline */
.highlight .cp { color: #cc0000; font-weight: bold } /* Comment.Preproc */
.highlight .cpf { color: #888888 } /* Comment.PreprocFile */
.highlight .c1 { color: #888888 } /* Comment.Single */
.highlight .cs { color: #cc0000; font-weight: bold; background-color: #fff0f0 } /* Comment.Special */
.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
.highlight .ge { font-style: italic } /* Generic.Emph */
.highlight .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */
.highlight .gr { color: #aa0000 } /* Generic.Error */
.highlight .gh { color: #333333 } /* Generic.Heading */
.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
.highlight .go { color: #888888 } /* Generic.Output */
.highlight .gp { color: #555555 } /* Generic.Prompt */
.highlight .gs { font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #666666 } /* Generic.Subheading */
.highlight .gt { color: #aa0000 } /* Generic.Traceback */
.highlight .kc { color: #008800; font-weight: bold } /* Keyword.Constant */
.highlight .kd { color: #008800; font-weight: bold } /* Keyword.Declaration */
.highlight .kn { color: #008800; font-weight: bold } /* Keyword.Namespace */
.highlight .kp { color: #008800 } /* Keyword.Pseudo */
.highlight .kr { color: #008800; font-weight: bold } /* Keyword.Reserved */
.highlight .kt { color: #888888; font-weight: bold } /* Keyword.Type */
.highlight .m { color: #0000DD; font-weight: bold } /* Literal.Number */
.highlight .s { color: #dd2200; background-color: #fff0f0 } /* Literal.String */
.highlight .na { color: #336699 } /* Name.Attribute */
.highlight .nb { color: #003388 } /* Name.Builtin */
.highlight .nc { c
#!/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=(",", ": "))