#!/usr/bin/python3 # Copyright © 2009 James Westby , # 2010, 2011 Stefano Rivera # # ################################################################## # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # # ################################################################## # pylint: disable=invalid-name # pylint: enable=invalid-name import argparse import logging import re import sys import webbrowser from collections.abc import Iterable from email.message import EmailMessage import debianbts from launchpadlib.launchpad import Launchpad from ubuntutools import getLogger from ubuntutools.config import UDTConfig Logger = getLogger() ATTACHMENT_MAX_SIZE = 2000 def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser() parser.add_argument( "-b", "--browserless", action="store_true", help="Don't open the bug in the browser at the end", ) parser.add_argument( "-l", "--lpinstance", metavar="INSTANCE", help="LP instance to connect to (default: production)", ) parser.add_argument( "-v", "--verbose", action="store_true", help="Print info about the bug being imported" ) parser.add_argument( "-n", "--dry-run", action="store_true", help="Don't actually open a bug (also sets verbose)", ) parser.add_argument( "-p", "--package", help="Launchpad package to file bug against (default: Same as Debian)" ) parser.add_argument( "--no-conf", action="store_true", help="Don't read config files or environment variables." ) parser.add_argument("bugs", nargs="+", help="Bug number(s) or URL(s)") return parser.parse_args() def get_bug_numbers(bug_list: Iterable[str]) -> list[int]: bug_re = re.compile(r"bug=(\d+)") bug_nums = [] for bug_num in bug_list: if bug_num.startswith("http"): # bug URL match = bug_re.search(bug_num) if match is None: Logger.error("Can't determine bug number from %s", bug_num) sys.exit(1) bug_num = match.groups()[0] bug_num = bug_num.lstrip("#") bug_nums.append(int(bug_num)) return bug_nums def walk_multipart_message(message: EmailMessage) -> tuple[str, list[tuple[int, EmailMessage]]]: summary = "" attachments = [] i = 1 for part in message.walk(): content_type = part.get_content_type() if content_type.startswith("multipart/"): # we're already iterating on multipart items # let's just skip the multipart extra metadata continue if content_type == "application/pgp-signature": # we're not interested in importing pgp signatures continue if part.is_attachment(): attachments.append((i, part)) elif content_type.startswith("image/"): # images here are not attachment, they are inline, but Launchpad can't handle that, # so let's add them as attachments summary += f"Message part #{i}\n" summary += f"[inline image '{part.get_filename()}']\n\n" attachments.append((i, part)) elif content_type.startswith("text/html"): summary += f"Message part #{i}\n" summary += "[inline html]\n\n" attachments.append((i, part)) elif content_type == "text/plain": summary += f"Message part #{i}\n" summary += part.get_content() + "\n" else: raise RuntimeError( f"""Unknown message part Your Debian bug is too weird to be imported in Launchpad, sorry. You can fix that by patching this script in ubuntu-dev-tools. Faulty message part: {part}""" ) i += 1 return summary, attachments def process_bugs( bugs: Iterable[debianbts.Bugreport], launchpad: Launchpad, package: str, dry_run: bool = True, browserless: bool = False, ) -> bool: debian = launchpad.distributions["debian"] ubuntu = launchpad.distributions["ubuntu"] lp_debbugs = launchpad.bug_trackers.getByName(name="debbugs") err = False for bug in bugs: ubupackage = bug.source if package: ubupackage = package bug_num = bug.bug_num subject = bug.subject log = debianbts.get_bug_log(bug_num) message = log[0]["message"] assert isinstance(message, EmailMessage) attachments: list[tuple[int, EmailMessage]] = [] if message.is_multipart(): summary, attachments = walk_multipart_message(message) else: summary = str(message.get_payload()) target = ubuntu.getSourcePackage(name=ubupackage) if target is None: Logger.error( "Source package '%s' is not in Ubuntu. Please specify " "the destination source package with --package", ubupackage, ) err = True continue description = f"Imported from Debian bug http://bugs.debian.org/{bug_num}:\n\n{summary}" # LP limits descriptions to 50K chars description = (description[:49994] + " [...]") if len(description) > 50000 else description Logger.debug("Target: %s", target) Logger.debug("Subject: %s", subject) Logger.debug("Description: ") Logger.debug(description) for i, attachment in attachments: Logger.debug("Attachment #%s (%s)", i, attachment.get_filename() or "inline") Logger.debug("Content:") if attachment.get_content_type() == "text/plain": content = attachment.get_content() if len(content) > ATTACHMENT_MAX_SIZE: content = ( content[:ATTACHMENT_MAX_SIZE] + f" [attachment cropped after {ATTACHMENT_MAX_SIZE} characters...]" ) Logger.debug(content) else: Logger.debug("[data]") if dry_run: Logger.info("Dry-Run: not creating Ubuntu bug.") continue u_bug = launchpad.bugs.createBug(target=target, title=subject, description=description) for i, attachment in attachments: name = f"#{i}-{attachment.get_filename() or "inline"}" content = attachment.get_content() if isinstance(content, str): # Launchpad only wants bytes content = content.encode() u_bug.addAttachment( filename=name, data=content, comment=f"Imported from Debian bug http://bugs.debian.org/{bug_num}", ) d_sp = debian.getSourcePackage(name=package) if d_sp is None and package: d_sp = debian.getSourcePackage(name=package) d_task = u_bug.addTask(target=d_sp) d_watch = u_bug.addWatch(remote_bug=bug_num, bug_tracker=lp_debbugs) d_task.bug_watch = d_watch d_task.lp_save() Logger.info("Opened %s", u_bug.web_link) if not browserless: webbrowser.open(u_bug.web_link) return err def main() -> None: options = parse_args() config = UDTConfig(options.no_conf) if options.lpinstance is None: options.lpinstance = config.get_value("LPINSTANCE") if options.dry_run: launchpad = Launchpad.login_anonymously("ubuntu-dev-tools") options.verbose = True else: launchpad = Launchpad.login_with("ubuntu-dev-tools", options.lpinstance) if options.verbose: Logger.setLevel(logging.DEBUG) bugs = debianbts.get_status(get_bug_numbers(options.bugs)) if not bugs: Logger.error("Cannot find any of the listed bugs") sys.exit(1) if process_bugs(bugs, launchpad, options.package, options.dry_run, options.browserless): sys.exit(1) if __name__ == "__main__": main()