2022-03-22 13:42:49 +01:00
|
|
|
import datetime
|
2022-10-13 14:09:59 +02:00
|
|
|
import enum
|
2022-03-21 14:11:38 +01:00
|
|
|
import logging
|
2022-03-22 13:42:49 +01:00
|
|
|
import os
|
2022-10-13 14:09:59 +02:00
|
|
|
import re
|
2022-03-21 14:11:38 +01:00
|
|
|
import sys
|
2022-10-13 14:09:59 +02:00
|
|
|
import tempfile
|
|
|
|
import textwrap
|
|
|
|
from typing import Dict, List, Optional
|
|
|
|
|
2022-03-22 13:42:49 +01:00
|
|
|
from lxml import etree as ET
|
2022-10-13 14:09:59 +02:00
|
|
|
from osc.core import Package, checkout_package, http_GET, makeurl
|
2022-03-22 13:42:49 +01:00
|
|
|
|
2022-03-21 14:11:38 +01:00
|
|
|
from osclib.comments import CommentAPI
|
2022-10-13 15:13:16 +02:00
|
|
|
from osclib.core import devel_project_get
|
2022-03-21 14:11:38 +01:00
|
|
|
|
|
|
|
MARKER = 'PackageListDiff'
|
|
|
|
|
|
|
|
|
2022-10-13 14:09:59 +02:00
|
|
|
class PkglistSectionCommend(enum.Enum):
|
|
|
|
ADD = "add"
|
|
|
|
REMOVE = "remove"
|
|
|
|
MOVE = "move"
|
|
|
|
|
|
|
|
|
|
|
|
class PkglistSection:
|
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
command: PkglistSectionCommend,
|
|
|
|
pkgs: Optional[List[str]] = None,
|
|
|
|
to_module: Optional[List[str]] = None,
|
|
|
|
from_module: Optional[List[str]] = None
|
|
|
|
):
|
|
|
|
self.command = command
|
|
|
|
if pkgs is None:
|
|
|
|
self.pkgs = []
|
|
|
|
else:
|
|
|
|
self.pkgs = pkgs
|
|
|
|
if pkgs is None:
|
|
|
|
self.to_module = []
|
|
|
|
else:
|
|
|
|
self.to_module = to_module
|
|
|
|
if pkgs is None:
|
|
|
|
self.from_module = []
|
|
|
|
else:
|
|
|
|
self.from_module = from_module
|
|
|
|
|
|
|
|
|
|
|
|
class PkglistComments:
|
2022-03-21 14:11:38 +01:00
|
|
|
"""Handling staging comments of diffs"""
|
|
|
|
|
2022-10-13 14:09:59 +02:00
|
|
|
def __init__(self, apiurl: str):
|
2022-03-21 14:11:38 +01:00
|
|
|
self.apiurl = apiurl
|
|
|
|
self.comment = CommentAPI(apiurl)
|
|
|
|
|
2022-10-10 09:37:23 +02:00
|
|
|
def read_summary_file(self, file: str) -> Dict[str, List[str]]:
|
2022-03-21 14:11:38 +01:00
|
|
|
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
|
|
|
|
|
2022-10-10 09:37:23 +02:00
|
|
|
def write_summary_file(self, file: str, content: dict):
|
2022-03-21 14:11:38 +01:00
|
|
|
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')
|
|
|
|
|
2022-10-10 09:37:23 +02:00
|
|
|
def calculcate_package_diff(self, old_file: str, new_file: str):
|
2022-03-21 14:11:38 +01:00
|
|
|
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()
|
|
|
|
|
2022-10-10 09:37:23 +02:00
|
|
|
def handle_package_diff(self, project: str, old_file: str, new_file: str):
|
2022-03-21 14:11:38 +01:00
|
|
|
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
|
2022-03-23 09:55:42 +01:00
|
|
|
if ct.startswith('approve ') or ct == 'approve':
|
|
|
|
print(c)
|
|
|
|
return 0
|
2022-03-21 14:11:38 +01:00
|
|
|
|
|
|
|
return 1
|
|
|
|
|
2022-10-14 15:33:39 +02:00
|
|
|
def is_approved(self, comment, comments: dict) -> Optional[str]:
|
2022-03-22 13:42:49 +01:00
|
|
|
if not comment:
|
|
|
|
return None
|
|
|
|
|
2022-03-21 14:11:38 +01:00
|
|
|
for c in comments.values():
|
|
|
|
if c['parent'] == comment['id']:
|
|
|
|
ct = c['comment']
|
|
|
|
if ct.startswith('approve ') or ct == 'approve':
|
2022-03-22 13:42:49 +01:00
|
|
|
return c['who']
|
|
|
|
return None
|
2022-03-21 14:11:38 +01:00
|
|
|
|
2022-10-13 14:09:59 +02:00
|
|
|
def parse_title(self, line: str) -> Optional[PkglistSection]:
|
2022-03-21 14:11:38 +01:00
|
|
|
m = re.match(r'\*\*Add to (.*)\*\*', line)
|
|
|
|
if m:
|
2022-10-13 14:09:59 +02:00
|
|
|
return PkglistSection(PkglistSectionCommend.ADD, pkgs=[], to_module=m.group(1).split(','))
|
2022-03-21 14:11:38 +01:00
|
|
|
m = re.match(r'\*\*Move from (.*) to (.*)\*\*', line)
|
|
|
|
if m:
|
2022-10-13 14:09:59 +02:00
|
|
|
return PkglistSection(
|
|
|
|
PkglistSectionCommend.MOVE,
|
|
|
|
pkgs=[],
|
|
|
|
from_module=m.group(1).split(','),
|
|
|
|
to_module=m.group(2).split(','),
|
|
|
|
)
|
2022-03-21 14:11:38 +01:00
|
|
|
m = re.match(r'\*\*Remove from (.*)\*\*', line)
|
|
|
|
if m:
|
2022-10-13 14:09:59 +02:00
|
|
|
return PkglistSection(
|
|
|
|
PkglistSectionCommend.REMOVE,
|
|
|
|
pkgs=[],
|
|
|
|
from_module=m.group(1).split(','),
|
|
|
|
)
|
2022-03-21 14:11:38 +01:00
|
|
|
return None
|
|
|
|
|
2022-10-13 14:09:59 +02:00
|
|
|
def parse_sections(self, comment: str) -> List[PkglistSection]:
|
2022-03-21 14:11:38 +01:00
|
|
|
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:
|
2022-10-13 14:09:59 +02:00
|
|
|
current_section.pkgs.append(pkg)
|
2022-03-21 14:11:38 +01:00
|
|
|
if current_section:
|
|
|
|
sections.append(current_section)
|
|
|
|
return sections
|
|
|
|
|
2022-10-13 14:09:59 +02:00
|
|
|
def apply_move(self, content: Dict[str, List[str]], section: PkglistSection):
|
|
|
|
for pkg in section.pkgs:
|
2022-03-21 14:11:38 +01:00
|
|
|
pkg_content = content[pkg]
|
2022-10-13 14:09:59 +02:00
|
|
|
for group in section.from_module:
|
2022-03-21 14:11:38 +01:00
|
|
|
try:
|
|
|
|
pkg_content.remove(group)
|
|
|
|
except ValueError:
|
|
|
|
logging.error(f"Can't remove {pkg} from {group}, not there. Mismatch.")
|
|
|
|
sys.exit(1)
|
2022-10-13 14:09:59 +02:00
|
|
|
for group in section.to_module:
|
2022-03-21 14:11:38 +01:00
|
|
|
pkg_content.append(group)
|
|
|
|
content[pkg] = pkg_content
|
|
|
|
|
2022-10-13 14:09:59 +02:00
|
|
|
def apply_add(self, content: Dict[str, List[str]], section: PkglistSection):
|
|
|
|
for pkg in section.pkgs:
|
2022-03-21 14:11:38 +01:00
|
|
|
content.setdefault(pkg, [])
|
2022-10-13 14:09:59 +02:00
|
|
|
content[pkg] += section.to_module
|
2022-03-21 14:11:38 +01:00
|
|
|
|
2022-10-13 14:09:59 +02:00
|
|
|
def apply_remove(self, content: Dict[str, List[str]], section: PkglistSection):
|
|
|
|
for pkg in section.pkgs:
|
2022-03-21 14:11:38 +01:00
|
|
|
pkg_content = content[pkg]
|
2022-10-13 14:09:59 +02:00
|
|
|
for group in section.from_module:
|
2022-03-21 14:11:38 +01:00
|
|
|
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
|
|
|
|
|
2022-10-13 14:09:59 +02:00
|
|
|
def apply_commands(self, filename: str, sections: List[PkglistSection]):
|
2022-03-21 14:11:38 +01:00
|
|
|
content = self.read_summary_file(filename)
|
|
|
|
for section in sections:
|
2022-10-13 14:09:59 +02:00
|
|
|
if section.command == PkglistSectionCommend.MOVE:
|
2022-03-21 14:11:38 +01:00
|
|
|
self.apply_move(content, section)
|
2022-10-13 14:09:59 +02:00
|
|
|
elif section.command == PkglistSectionCommend.ADD:
|
2022-03-21 14:11:38 +01:00
|
|
|
self.apply_add(content, section)
|
2022-10-13 14:09:59 +02:00
|
|
|
elif section.command == PkglistSectionCommend.REMOVE:
|
2022-03-21 14:11:38 +01:00
|
|
|
self.apply_remove(content, section)
|
|
|
|
self.write_summary_file(filename, content)
|
|
|
|
|
2022-10-13 14:09:59 +02:00
|
|
|
def format_pkgs(self, pkgs: List[str]):
|
2022-03-22 13:42:49 +01:00
|
|
|
text = ', '.join(pkgs)
|
|
|
|
return " " + "\n ".join(textwrap.wrap(text, width=68, break_long_words=False, break_on_hyphens=False)) + "\n\n"
|
|
|
|
|
2022-10-13 14:09:59 +02:00
|
|
|
def format_move(self, section: PkglistSection):
|
|
|
|
gfrom = ','.join(section.from_module)
|
|
|
|
gto = ','.join(section.to_module)
|
2022-03-22 13:42:49 +01:00
|
|
|
text = f" * Move from {gfrom} to {gto}:\n"
|
2022-10-13 14:09:59 +02:00
|
|
|
return text + self.format_pkgs(section.pkgs)
|
2022-03-22 13:42:49 +01:00
|
|
|
|
2022-10-13 14:09:59 +02:00
|
|
|
def format_add(self, section: PkglistSection):
|
|
|
|
gto = ','.join(section.to_module)
|
2022-03-22 13:42:49 +01:00
|
|
|
text = f" * Add to {gto}:\n"
|
2022-10-13 14:09:59 +02:00
|
|
|
return text + self.format_pkgs(section.pkgs)
|
2022-03-22 13:42:49 +01:00
|
|
|
|
2022-10-13 14:09:59 +02:00
|
|
|
def format_remove(self, section: PkglistSection):
|
|
|
|
gfrom = ','.join(section.from_module)
|
2022-03-22 13:42:49 +01:00
|
|
|
text = f" * Remove from {gfrom}:\n"
|
2022-10-13 14:09:59 +02:00
|
|
|
return text + self.format_pkgs(section.pkgs)
|
2022-03-22 13:42:49 +01:00
|
|
|
|
2022-10-13 14:09:59 +02:00
|
|
|
def apply_changes(self, filename: str, sections: List[PkglistSection], approver: str):
|
2022-03-22 13:42:49 +01:00
|
|
|
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:
|
2022-10-13 14:09:59 +02:00
|
|
|
if section.command == PkglistSectionCommend.MOVE:
|
2022-03-22 13:42:49 +01:00
|
|
|
text += self.format_move(section)
|
2022-10-13 14:09:59 +02:00
|
|
|
elif section.command == PkglistSectionCommend.ADD:
|
2022-03-22 13:42:49 +01:00
|
|
|
text += self.format_add(section)
|
2022-10-13 14:09:59 +02:00
|
|
|
elif section.command == PkglistSectionCommend.REMOVE:
|
2022-03-22 13:42:49 +01:00
|
|
|
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)
|
|
|
|
|
2022-10-10 09:37:23 +02:00
|
|
|
def check_staging_accept(self, project: str, target: str):
|
2022-03-21 14:11:38 +01:00
|
|
|
comments = self.comment.get_comments(project_name=project)
|
|
|
|
comment, _ = self.comment.comment_find(comments, MARKER)
|
2022-03-22 13:42:49 +01:00
|
|
|
approver = self.is_approved(comment, comments)
|
|
|
|
if not approver:
|
2022-03-21 14:11:38 +01:00
|
|
|
return
|
|
|
|
sections = self.parse_sections(comment['comment'])
|
2022-10-13 15:13:16 +02:00
|
|
|
project, package = devel_project_get(self.apiurl, target, '000package-groups')
|
|
|
|
if project is None or package is None:
|
|
|
|
raise ValueError('Could not determine devel project or package for the "000package-groups"!')
|
2022-03-21 14:11:38 +01:00
|
|
|
with tempfile.TemporaryDirectory() as tmpdirname:
|
2022-10-13 15:13:16 +02:00
|
|
|
checkout_package(self.apiurl, project, package, expand_link=True, outdir=tmpdirname)
|
2022-03-21 14:11:38 +01:00
|
|
|
self.apply_commands(tmpdirname + '/summary-staging.txt', sections)
|
2022-03-22 13:42:49 +01:00
|
|
|
self.apply_changes(tmpdirname + '/package-groups.changes', sections, approver)
|
|
|
|
package = Package(tmpdirname)
|
|
|
|
package.commit(msg='Approved packagelist changes', skip_local_service_run=True)
|