#
# RTEMS Tools Project (http://www.rtems.org/)
# Copyright 2018 Danxue Huang (danxue.huang@gmail.com)
# Copyright 2022 Chris Johns (chris@contemporary.software)
# All rights reserved.
#
# This file is part of the RTEMS Tools package in 'rtems-tools'.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
import datetime
import os
import re
import time
import threading
import sys
from markdown_generator import MarkdownGenerator
import reraise
heading_base = 2
class ticket(object):
def __init__(self, fmt, ticket):
self.generator = MarkdownGenerator()
self.format = fmt
self.ticket = ticket
self.thread = None
self.result = None
def _format_contents(self):
ticket_meta = self.ticket['meta']
ticket_link = self.ticket.get('comment_attachment',
{}).get('link', None)
summary = ticket_meta.get('summary', None)
ticket_meta.pop('description', None)
ticket_meta.pop('summary', None)
if ticket_link is not None:
ticket_id_link = \
self.generator.gen_hyperlink(self.ticket_id(), '#t' + self.ticket_id())
tlink = self.generator.gen_bold(ticket_id_link) + \
' - ' + self.generator.gen_bold(summary)
self.generator.gen_heading(tlink,
heading_base + 1,
anchor='t' + self.ticket_id())
for k in ['Created', 'Modified', 'Blocked By']:
ticket_meta[k] = self.ticket['ticket'][k]
meta_keys = [k.capitalize() for k in ticket_meta.keys()]
meta_vals = [v for v in ticket_meta.values()]
order = [
'Id', 'Reporter', 'Created', 'Modified', 'Owner', 'Type',
'Component', 'Status', 'Resolution', 'Version', 'Milestone',
'Priority', 'Severity', 'Keywords', 'Cc', 'Blocking', 'Blocked by'
]
meta_table = []
for c in range(0, len(order)):
i = meta_keys.index(order[c])
if meta_keys[i] in ['Created', 'Modified']:
dt = datetime.datetime.strptime(meta_vals[i],
'%m/%d/%y %H:%M:%S')
ds = dt.strftime('%d %B %Y %H:%M:%S')
if ds[0] == '0':
ds = ds[1:]
meta_vals[i] = ds
meta_table += [[
self.generator.gen_bold(meta_keys[i]), meta_vals[i]
]]
meta_table = [[
self.generator.gen_bold('Link'),
self.generator.gen_hyperlink(ticket_link, ticket_link)
]] + meta_table
self.generator.gen_table(None, meta_table, align=['right', 'left'])
def _description(self, description):
description = description.replace('\r\n', '\n')
#
# The code blocks needs to be reviewed
#
if self.ticket_id() == '3384':
description = re.sub('%s}}}', '%s}\n}\n}', description)
if self.format == 'markdown':
description = re.sub(r'{{{(.*)}}}', r'`\1`', description)
else:
description = re.sub(r'{{{(.*)}}}', r':code:`\1`', description)
if self.format == 'rst':
description = re.sub(r'(>+) ---', r'\1 \-\-\-', description)
description = re.sub(r'{{{!(.*)\n', '{{{\n', description)
description = re.sub(r'}}}}', '}\n}}}', description)
description = re.sub(r'{{{[ \t]+\n', '{{{\n', description)
description = re.sub(r'{{{([#$])', '{{{\n#', description)
description = description.replace('{{{\n', '```\n')
description = description.replace('\n}}}', '\n```')
description = re.sub(
r"^[ \t]*#([ \t]*define|[ \t]*include|[ \t]*endif|[ \t]*ifdef|" \
"[ \t]*ifndef|[ \t]*if|[ \t]*else)(.*)$",
r"`#\1\2`",
description,
flags=re.MULTILINE)
if self.format == 'markdown':
description = re.sub(r'{{{(?!\n)', '`', description)
description = re.sub(r'(?!\n)}}}', '`', description)
else:
description = re.sub(r'{{{(?!\n)', ':code:`', description)
description = re.sub(r'(?!\n)}}}', '`', description)
# Two lines after the opening (and after the ending)
# back-ticks misses up with the text area rendering.
description = re.sub('```\n\n', '```\n', description)
description = re.sub('\n\n```', '\n```', description)
# For ticket 2624 where the opening three curly braces are not
# on a separate line.
description = re.sub(r'```(?!\n)', '```\n', description)
description = re.sub(r'(?!\n)```', '\n```', description)
# For ticket 2993 where the defective closing curly brackets
# miss up with text area rendering.
description = re.sub('}}:', '```\n', description)
# Ticket 3771 has code that's not written in a code block,
# which is interpretted by the Markdown generator as headers
# (#define)... Hence, we fix that manually.
if self.ticket_id() == '3771':
description = re.sub('`#define',
'```\n#define',
description,
count=1)
description = re.sub('Problem facing on writing',
'```\nProblem facing on writing',
description,
count=1)
description = re.sub(r'[ ]{8,}', ' ', description)
if self.format == 'rst':
description = description.replace('=', '\\=')
description = description.replace('\n', '\n\n')
description = re.sub(r'^(#+)', '', description, flags=re.MULTILINE)
return description
def _format_description(self):
if 'description' not in self.ticket['comment_attachment']:
return
description = self.ticket['comment_attachment']['description']
self.generator.gen_raw_text(self.generator.gen_bold('Description'))
self.generator.gen_line('')
self.generator.gen_line_block(self._description(description))
self.generator.gen_line('')
def _meta_label(self, label):
if label == 'attachment':
label = 'attach'
return label
def _format_comments(self):
if 'comments' not in self.ticket['comment_attachment']:
return
comments = self.ticket['comment_attachment']['comments']
if len(comments) == 0:
return
self.generator.gen_line('')
cnt = 0
bold = self.generator.gen_bold
for comment in comments:
cnt += 1
self.generator.gen_line(
self.generator.gen_topic('Comment ' + str(cnt)))
self.generator.gen_line('')
if not comment['creator']:
creator = 'none'
else:
creator = comment['creator']
ul = [bold(creator) + ', ' + comment['published']]
for m in comment['meta']:
ul += [bold(self._meta_label(m[0]) + ':') + ' ' + m[1]]
self.generator.gen_raw(self.generator.gen_ordered_lists(ul))
self.generator.gen_line('')
self.generator.gen_line_block(
self._description(comment['description']))
self.generator.gen_line('')
def _format_attachments(self):
if 'attachments' not in self.ticket['comment_attachment']:
return
attachments = self.ticket['comment_attachment']['attachments']
if len(attachments) == 0:
return
self.generator.gen_heading('Attachments:', heading_base + 2)
cnt = 0
tab = []
bold = self.generator.gen_bold
for attachment in attachments:
cnt += 1
tab += [[
bold(str(cnt)),
bold('%s, %s' %
(attachment['creator'], attachment['published']))
]]
for m in attachment['meta']:
tab += [['', bold(self._meta_label(m[0])) + ': ' + m[1]]]
if len(attachment['description']) != 0:
tab += [['', attachment['description']]]
if len(tab) != 0:
self.generator.gen_line('')
self.generator.gen_table(None, tab)
self.generator.gen_line('')
def _runner(self):
try:
self.formatter()
except KeyboardInterrupt:
pass
except:
self.result = sys.exc_info()
def formatter(self):
self._format_contents()
self._format_description()
self._format_attachments()
self._format_comments()
def ticket_id(self):
return self.ticket['ticket']['id']
def run(self):
self.thread = threading.Thread(target=self._runner,
name='format-ticket-%s' %
(self.ticket_id()))
self.thread.start()
def is_alive(self):
return self.thread and self.thread.is_alive()
def reraise(self):
if self.result is not None:
reraise.reraise(*self.result)
class generator:
def __init__(self, release, fmt='markdown'):
if fmt != 'markdown' and fmt != 'trac':
raise RuntimeError('invalid format: ' + fmt)
self.release = release
self.milestone = None
self.format = fmt
self.generator = MarkdownGenerator()
def set_milestone(self, milestone):
self.milestone = milestone
def gen_toc(self, notes):
headings = [line for line in notes
if line.startswith('##')] if notes is not None else []
self.generator.gen_raw(self.md_toc(headings))
def gen_start(self, notes):
self.generator.gen_raw('# RTEMS Release ' + self.milestone +
os.linesep)
if notes is not None:
self.generator.gen_raw(os.linesep.join(notes))
self.generator.gen_page_break()
def gen_overall_progress(self, overall_progress):
self.generator.gen_heading(
'RTEMS ' + self.milestone + ' Ticket Overview', heading_base)
self.generator.gen_table(
[k.capitalize() for k in overall_progress.keys()],
[overall_progress.values()],
align='left')
def gen_tickets_summary(self, tickets):
self.generator.gen_line_break()
self.generator.gen_heading(
'RTEMS ' + self.milestone + ' Ticket Summary', heading_base)
keys = tickets.keys()
id_summary_mapping = [
('[%s](#t%s)' % (k, k), tickets[k]['meta']['status'],
tickets[k]['meta']['summary']) for k in keys
]
cols = ['ID', 'Status', 'Summary']
self.generator.gen_table(cols, id_summary_mapping, sort_by='ID')
self.generator.gen_line_break()
@staticmethod
def _convert_to_bulleted_link(name: str, generator):
level = name.count('#')
stripped_name = name.replace('#', '').strip()
linked_name = name.lower().replace(' ',
'-').replace('-', '', 1).replace(
'#', '', level - 1)
if not isinstance(generator, MarkdownGenerator):
linked_name = linked_name.replace('.', '-')
return f"{(' ' * (level - 1)) + '* '}[{stripped_name}]({linked_name})"
def md_toc(self, headings):
tmp_gen = MarkdownGenerator()
toc_headers = [h[1:] for h in headings]
toc_headers.extend([
'# RTEMS ' + self.milestone + ' Ticket Overview',
'# RTEMS ' + self.milestone + ' Ticket Summary',
'# RTEMS ' + self.milestone + ' Tickets By Category'
])
toc_headers.append('# RTEMS ' + self.milestone + ' Tickets')
bulleted_links = []
for c in toc_headers:
bulleted_links.append(self._convert_to_bulleted_link(c, tmp_gen))
for b in bulleted_links:
tmp_gen.gen_unwrapped_line(b)
return tmp_gen.content
def gen_tickets_stats_by_category(self, by_category):
self.generator.gen_heading('RTEMS ' + self.milestone + \
' Tickets By Category', heading_base)
self.generator.gen_line('')
for category in by_category:
self.generator.gen_heading(category.capitalize(), heading_base + 1)
# Get header and all rows to generate table, set category as first col
header = [category.capitalize()]
rows = []
ticket_stats_list = list(by_category[category].values())
if len(ticket_stats_list) > 0:
header += [k.capitalize() for k in ticket_stats_list[0].keys()]
for category_value in by_category[category]:
ticket_stats = by_category[category][category_value]
rows.append([category_value] + list(ticket_stats.values()))
self.generator.gen_table(header, rows)
self.generator.gen_line('')
def gen_individual_tickets_info(self, tickets):
self.generator.gen_line_break()
self.generator.gen_heading('RTEMS ' + self.milestone + ' Tickets',
heading_base)
num_jobs = 1
job_count = 0
job_total = len(tickets)
job_len = len(str(job_total))
for ticket_id in sorted(list(tickets.keys())):
job = ticket(self.format, tickets[ticket_id])
job_count += 1
print('\r %*d of %d - %s ticket %s ' %
(job_len, job_count, job_total, self.milestone, ticket_id),
end='')
job.formatter()
self.generator.gen_horizontal_line()
self.generator.content += job.generator.content
print()