openSUSE-release-tools/osclib/pkglistgen_comments.py

278 lines
10 KiB
Python
Raw Normal View History

import datetime
import textwrap
import re
import tempfile
import logging
import os
import sys
from lxml import etree as ET
from osclib.comments import CommentAPI
from osc.core import checkout_package, http_GET, makeurl
from osc.core import Package
MARKER = 'PackageListDiff'
class PkglistComments(object):
"""Handling staging comments of diffs"""
def __init__(self, apiurl):
self.apiurl = apiurl
self.comment = CommentAPI(apiurl)
def read_summary_file(self, file):
ret = dict()
with open(file, 'r') as f:
for line in f:
pkg, group = line.strip().split(':')
ret.setdefault(pkg, [])
ret[pkg].append(group)
return ret
def write_summary_file(self, file, content):
output = []
for pkg in sorted(content):
for group in sorted(content[pkg]):
output.append(f"{pkg}:{group}")
with open(file, 'w') as f:
for line in sorted(output):
f.write(line + '\n')
def calculcate_package_diff(self, old_file, new_file):
old_file = self.read_summary_file(old_file)
new_file = self.read_summary_file(new_file)
# remove common part
keys = list(old_file.keys())
for key in keys:
if new_file.get(key, []) == old_file[key]:
del new_file[key]
del old_file[key]
if not old_file and not new_file:
return None
removed = dict()
for pkg in old_file:
old_groups = old_file[pkg]
if new_file.get(pkg):
continue
removekey = ','.join(old_groups)
removed.setdefault(removekey, [])
removed[removekey].append(pkg)
report = ''
for rm in sorted(removed.keys()):
report += f"**Remove from {rm}**\n\n```\n"
paragraph = ', '.join(removed[rm])
report += "\n".join(textwrap.wrap(paragraph, width=90, break_long_words=False, break_on_hyphens=False))
report += "\n```\n\n"
moved = dict()
for pkg in old_file:
old_groups = old_file[pkg]
new_groups = new_file.get(pkg)
if not new_groups:
continue
movekey = ','.join(old_groups) + ' to ' + ','.join(new_groups)
moved.setdefault(movekey, [])
moved[movekey].append(pkg)
for move in sorted(moved.keys()):
report += f"**Move from {move}**\n\n```\n"
paragraph = ', '.join(moved[move])
report += "\n".join(textwrap.wrap(paragraph, width=90, break_long_words=False, break_on_hyphens=False))
report += "\n```\n\n"
added = dict()
for pkg in new_file:
if pkg in old_file:
continue
addkey = ','.join(new_file[pkg])
added.setdefault(addkey, [])
added[addkey].append(pkg)
for group in sorted(added):
report += f"**Add to {group}**\n\n```\n"
paragraph = ', '.join(added[group])
report += "\n".join(textwrap.wrap(paragraph, width=90, break_long_words=False, break_on_hyphens=False))
report += "\n```\n\n"
return report.strip()
def handle_package_diff(self, project, old_file, new_file):
comments = self.comment.get_comments(project_name=project)
comment, _ = self.comment.comment_find(comments, MARKER)
report = self.calculcate_package_diff(old_file, new_file)
if not report:
if comment:
self.comment.delete(comment['id'])
return 0
report = self.comment.add_marker(report, MARKER)
if comment:
write_comment = report != comment['comment']
else:
write_comment = True
if write_comment:
if comment:
self.comment.delete(comment['id'])
self.comment.add_comment(project_name=project, comment=report)
else:
for c in comments.values():
if c['parent'] == comment['id']:
ct = c['comment']
if ct.startswith('ignore ') or ct == 'ignore':
print(c)
return 0
if ct.startswith('approve ') or ct == 'approve':
print(c)
return 0
return 1
def is_approved(self, comment, comments):
if not comment:
return None
for c in comments.values():
if c['parent'] == comment['id']:
ct = c['comment']
if ct.startswith('approve ') or ct == 'approve':
return c['who']
return None
def parse_title(self, line):
m = re.match(r'\*\*Add to (.*)\*\*', line)
if m:
return {'cmd': 'add', 'to': m.group(1).split(','), 'pkgs': []}
m = re.match(r'\*\*Move from (.*) to (.*)\*\*', line)
if m:
return {'cmd': 'move', 'from': m.group(1).split(','), 'to': m.group(2).split(','), 'pkgs': []}
m = re.match(r'\*\*Remove from (.*)\*\*', line)
if m:
return {'cmd': 'remove', 'from': m.group(1).split(','), 'pkgs': []}
return None
def parse_sections(self, comment):
current_section = None
sections = []
in_quote = False
for line in comment.split('\n'):
if line.startswith('**'):
if current_section:
sections.append(current_section)
current_section = self.parse_title(line)
continue
if line.startswith("```"):
in_quote = not in_quote
continue
if in_quote:
for pkg in line.split(','):
pkg = pkg.strip()
if pkg:
current_section['pkgs'].append(pkg)
if current_section:
sections.append(current_section)
return sections
def apply_move(self, content, section):
for pkg in section['pkgs']:
pkg_content = content[pkg]
for group in section['from']:
try:
pkg_content.remove(group)
except ValueError:
logging.error(f"Can't remove {pkg} from {group}, not there. Mismatch.")
sys.exit(1)
for group in section['to']:
pkg_content.append(group)
content[pkg] = pkg_content
def apply_add(self, content, section):
for pkg in section['pkgs']:
content.setdefault(pkg, [])
content[pkg] += section['to']
def apply_remove(self, content, section):
for pkg in section['pkgs']:
pkg_content = content[pkg]
for group in section['from']:
try:
pkg_content.remove(group)
except ValueError:
logging.error(f"Can't remove {pkg} from {group}, not there. Mismatch.")
sys.exit(1)
content[pkg] = pkg_content
def apply_commands(self, filename, sections):
content = self.read_summary_file(filename)
for section in sections:
if section['cmd'] == 'move':
self.apply_move(content, section)
elif section['cmd'] == 'add':
self.apply_add(content, section)
elif section['cmd'] == 'remove':
self.apply_remove(content, section)
self.write_summary_file(filename, content)
def format_pkgs(self, pkgs):
text = ', '.join(pkgs)
return " " + "\n ".join(textwrap.wrap(text, width=68, break_long_words=False, break_on_hyphens=False)) + "\n\n"
def format_move(self, section):
gfrom = ','.join(section['from'])
gto = ','.join(section['to'])
text = f" * Move from {gfrom} to {gto}:\n"
return text + self.format_pkgs(section['pkgs'])
def format_add(self, section):
gto = ','.join(section['to'])
text = f" * Add to {gto}:\n"
return text + self.format_pkgs(section['pkgs'])
def format_remove(self, section):
gfrom = ','.join(section['from'])
text = f" * Remove from {gfrom}:\n"
return text + self.format_pkgs(section['pkgs'])
def apply_changes(self, filename, sections, approver):
text = "-------------------------------------------------------------------\n"
now = datetime.datetime.utcnow()
date = now.strftime("%a %b %d %H:%M:%S UTC %Y")
url = makeurl(self.apiurl, ['person', approver])
root = ET.parse(http_GET(url))
realname = root.find('realname').text
email = root.find('email').text
text += f"{date} - {realname} <{email}>\n\n- Approved changes to summary-staging.txt\n"
for section in sections:
if section['cmd'] == 'move':
text += self.format_move(section)
elif section['cmd'] == 'add':
text += self.format_add(section)
elif section['cmd'] == 'remove':
text += self.format_remove(section)
with open(filename + '.new', 'w') as writer:
writer.write(text)
with open(filename, 'r') as reader:
for line in reader:
writer.write(line)
os.rename(filename + '.new', filename)
def check_staging_accept(self, project, target):
comments = self.comment.get_comments(project_name=project)
comment, _ = self.comment.comment_find(comments, MARKER)
approver = self.is_approved(comment, comments)
if not approver:
return
sections = self.parse_sections(comment['comment'])
with tempfile.TemporaryDirectory() as tmpdirname:
checkout_package(self.apiurl, target, '000package-groups', expand_link=True, outdir=tmpdirname)
self.apply_commands(tmpdirname + '/summary-staging.txt', sections)
self.apply_changes(tmpdirname + '/package-groups.changes', sections, approver)
package = Package(tmpdirname)
package.commit(msg='Approved packagelist changes', skip_local_service_run=True)