diff options
Diffstat (limited to 'tools/create_changelog.py')
-rw-r--r-- | tools/create_changelog.py | 248 |
1 files changed, 170 insertions, 78 deletions
diff --git a/tools/create_changelog.py b/tools/create_changelog.py index 7599cc6b9..6c24f417f 100644 --- a/tools/create_changelog.py +++ b/tools/create_changelog.py @@ -1,46 +1,13 @@ -############################################################################# -## -## Copyright (C) 2019 The Qt Company Ltd. -## Contact: https://www.qt.io/licensing/ -## -## This file is part of the Qt for Python project. -## -## $QT_BEGIN_LICENSE:LGPL$ -## Commercial License Usage -## Licensees holding valid commercial Qt licenses may use this file in -## accordance with the commercial license agreement provided with the -## Software or, alternatively, in accordance with the terms contained in -## a written agreement between you and The Qt Company. For licensing terms -## and conditions see https://www.qt.io/terms-conditions. For further -## information use the contact form at https://www.qt.io/contact-us. -## -## GNU Lesser General Public License Usage -## Alternatively, this file may be used under the terms of the GNU Lesser -## General Public License version 3 as published by the Free Software -## Foundation and appearing in the file LICENSE.LGPL3 included in the -## packaging of this file. Please review the following information to -## ensure the GNU Lesser General Public License version 3 requirements -## will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -## -## GNU General Public License Usage -## Alternatively, this file may be used under the terms of the GNU -## General Public License version 2.0 or (at your option) the GNU General -## Public license version 3 or any later version approved by the KDE Free -## Qt Foundation. The licenses are as published by the Free Software -## Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -## included in the packaging of this file. Please review the following -## information to ensure the GNU General Public License requirements will -## be met: https://www.gnu.org/licenses/gpl-2.0.html and -## https://www.gnu.org/licenses/gpl-3.0.html. -## -## $QT_END_LICENSE$ -## -############################################################################# +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only import re +import os import sys +import textwrap from argparse import ArgumentParser, Namespace, RawTextHelpFormatter -from subprocess import check_output, Popen, PIPE +from pathlib import Path +from subprocess import PIPE, Popen, check_output from typing import Dict, List, Tuple content_header = """Qt for Python @VERSION is a @TYPE release. @@ -59,15 +26,57 @@ Each of these identifiers can be entered in the bug tracker to obtain more information about a particular change. **************************************************************************** -* PySide2 * +* PySide6 * **************************************************************************** """ shiboken_header = """**************************************************************************** -* Shiboken2 * +* Shiboken6 * **************************************************************************** """ +description = """ +PySide6 changelog tool + +Example usage: +tools/create_changelog.py -v -r 6.5.3 +""" + + +def change_log(version: list) -> Path: + """Return path of the changelog of the version.""" + name = f"changes-{version[0]}.{version[1]}.{version[2]}" + return Path(__file__).parents[1] / "doc" / "changelogs" / name + + +def is_lts_version(version: list) -> bool: + return version[0] == 5 or version[1] in (2, 5) + + +def version_tag(version: list) -> str: + """Return the version tag.""" + tag = f"v{version[0]}.{version[1]}.{version[2]}" + return tag + "-lts" if is_lts_version(version) else tag + + +def revision_range(version: list) -> str: + """Determine a git revision_range from the version. Either log from + the previous version tag or since the last update to the changelog.""" + changelog = change_log(version) + if changelog.is_file(): + output = check_output(["git", "log", "-n", "1", "--format=%H", + os.fspath(changelog)]) + if output: + return output.strip().decode("utf-8") + "..HEAD" + + last_version = version.copy() + if version[2] == 0: + adjust_idx = 0 if version[1] == 0 else 1 + else: + adjust_idx = 2 + last_version[adjust_idx] -= 1 + return version_tag(last_version) + "..HEAD" + def parse_options() -> Namespace: tag_msg = ("Tags, branches, or SHA to compare\n" @@ -75,7 +84,7 @@ def parse_options() -> Namespace: " v5.12.0..v5.12.1\n" " cebc32a5..5.12") - options = ArgumentParser(description="PySide2 changelog tool", + options = ArgumentParser(description=description, formatter_class=RawTextHelpFormatter) options.add_argument("-d", "--directory", @@ -84,8 +93,7 @@ def parse_options() -> Namespace: options.add_argument("-v", "--versions", type=str, - help=tag_msg, - required=True) + help=tag_msg) options.add_argument("-r", "--release", type=str, @@ -94,18 +102,52 @@ def parse_options() -> Namespace: options.add_argument("-t", "--type", type=str, - help="Release type: bug-fix, minor, or major", - default="bug-fix") + help="Release type: bug-fix, minor, or major") + + options.add_argument("-e", + "--exclude", + action="store_true", + help="Exclude commits with a 'Pick-to' line", + default=False) args = options.parse_args() + + release_version = list(int(v) for v in args.release.split(".")) + if len(release_version) != 3: + print("Error: --release must be of form major.minor.patch") + sys.exit(-1) + + # Some auto-detection smartness + if not args.type: + if release_version[2] == 0: + args.type = "major" if release_version[1] == 0 else "minor" + else: + args.type = "bug-fix" + # For major/minor releases, skip all fixes with "Pick-to: " since they + # appear in bug-fix releases. + if args.type != "bug-fix": + args.exclude = True + print(f'Assuming "{args.type}" version', file=sys.stderr) + if args.type not in ("bug-fix", "minor", "major"): - print("Error:" + print("Error: " "-y/--type needs to be: bug-fix (default), minor, or major") sys.exit(-1) + if not args.versions: + args.versions = revision_range(release_version) + print(f"Assuming range {args.versions}", file=sys.stderr) + + args.release_version = release_version return args +def format_text(text: str) -> str: + """Format an entry with a leading dash, 80 columns""" + return textwrap.fill(text, width=77, initial_indent=" - ", + subsequent_indent=" ") + + def check_tag(tag: str) -> bool: output = False @@ -140,17 +182,35 @@ def get_commit_content(sha: str) -> str: print(err, file=sys.stderr) return out.decode("utf-8") + def git_get_sha1s(versions: List[str], pattern: str): """Return a list of SHA1s matching a pattern""" command = "git rev-list --reverse --grep '^{}'".format(pattern) command += " {}..{}".format(versions[0], versions[1]) command += " | git cat-file --batch" - command += " | grep -o -E \"^[0-9a-f]{40}\"" + command += " | grep -o -E \"^[0-9a-f]{40} commit\"" + command += " | awk '{print $1}'" print("{}: {}".format(git_command.__name__, command), file=sys.stderr) out_sha1, err = Popen(command, stdout=PIPE, shell=True).communicate() if err: print(err, file=sys.stderr) - return [s.decode("utf-8") for s in out_sha1.splitlines()] + + pick_to_sha1 = [] + + if exclude_pick_to: + # if '-e', we exclude all the 'Pick-to' changes + command = "git rev-list --reverse --grep '^Pick-to:'" + command += " {}..{}".format(versions[0], versions[1]) + command += " | git cat-file --batch" + command += " | grep -o -E \"^[0-9a-f]{40} commit\"" + command += " | awk '{print $1}'" + print("{}: {}".format(git_command.__name__, command), file=sys.stderr) + out_e_sha1, err = Popen(command, stdout=PIPE, shell=True).communicate() + if err: + print(err, file=sys.stderr) + pick_to_sha1 = out_e_sha1.splitlines() + + return [s.decode("utf-8") for s in out_sha1.splitlines() if s not in pick_to_sha1] def git_command(versions: List[str], pattern: str): @@ -174,11 +234,11 @@ def git_command(versions: List[str], pattern: str): task_number = int(task_number_match.group(1)) entry = {"title": title, "task": task, "task-number": task_number} if "shiboken" in title: - if sha not in shiboken2_commits: - shiboken2_commits[sha] = entry + if sha not in shiboken6_commits: + shiboken6_commits[sha] = entry else: - if sha not in pyside2_commits: - pyside2_commits[sha] = entry + if sha not in pyside6_commits: + pyside6_commits[sha] = entry def create_fixes_log(versions: List[str]) -> None: @@ -189,18 +249,19 @@ def create_task_log(versions: List[str]) -> None: git_command(versions, "Task-number: ") -def extract_change_log(commit_message: List[str]) -> Tuple[str, List[str]]: - """Extract a tuple of (component, change log lines) from a commit message - of the form [ChangeLog][shiboken2] description...""" - result = [] +def extract_change_log(commit_message: List[str]) -> Tuple[str, int, str]: + """Extract a tuple of (component, task-number, change log paragraph) + from a commit message of the form [ChangeLog][shiboken6] description...""" + result = '' component = 'pyside' within_changelog = False + task_nr = '' for line in commit_message: if within_changelog: if line: - result.append(' ' + line.strip()) + result += ' ' + line.strip() else: - break + within_changelog = False else: if line.startswith('[ChangeLog]'): log_line = line[11:] @@ -209,36 +270,63 @@ def extract_change_log(commit_message: List[str]) -> Tuple[str, List[str]]: if end > 0: component = log_line[1:end] log_line = log_line[end + 1:] - result.append(' * ' + log_line.strip()) + result = log_line.strip() within_changelog = True - return (component, result) + elif line.startswith("Fixes: ") or line.startswith("Task-number: "): + task_nr = line.split(":")[1].strip() + + task_nr_int = -1 + if task_nr: + result = f"[{task_nr}] {result}" + dash = task_nr.find('-') # "PYSIDE-627" + task_nr_int = int(task_nr[dash + 1:]) + + return (component, task_nr_int, format_text(result)) def create_change_log(versions: List[str]) -> None: for sha in git_get_sha1s(versions, r"\[ChangeLog\]"): change_log = extract_change_log(get_commit_content(sha).splitlines()) - if change_log[0].startswith('shiboken'): - shiboken2_changelogs.extend(change_log[1]) + component, task_nr, text = change_log + if component.startswith('shiboken'): + shiboken6_changelogs.append((task_nr, text)) else: - pyside2_changelogs.extend(change_log[1]) + pyside6_changelogs.append((task_nr, text)) + + +def format_commit_msg(entry: Dict[str, str]) -> str: + task = entry["task"].replace("Fixes: ", "").replace("Task-number: ", "") + title = entry["title"] + if title.startswith("shiboken6: "): + title = title[11:] + elif title.startswith("PySide6: "): + title = title[9:] + return format_text(f"[{task}] {title}") def gen_list(d: Dict[str, Dict[str, str]]) -> str: - return "".join(" - [{}] {}\n".format(v["task"], v["title"]) - for _, v in d.items()) + return "\n".join(format_commit_msg(v) + for _, v in d.items()) def sort_dict(d: Dict[str, Dict[str, str]]) -> Dict[str, Dict[str, str]]: return dict(sorted(d.items(), key=lambda kv: kv[1]['task-number'])) +def sort_changelog(c: List[Tuple[int, str]]) -> List[Tuple[int, str]]: + return sorted(c, key=lambda task_text_tuple: task_text_tuple[0]) + + if __name__ == "__main__": args = parse_options() - pyside2_commits: Dict[str, Dict[str, str]] = {} - shiboken2_commits: Dict[str, Dict[str, str]] = {} - pyside2_changelogs: List[str] = [] - shiboken2_changelogs: List[str] = [] + pyside6_commits: Dict[str, Dict[str, str]] = {} + shiboken6_commits: Dict[str, Dict[str, str]] = {} + # Changelogs are tuples of task number/formatted text + pyside6_changelogs: List[Tuple[int, str]] = [] + shiboken6_changelogs: List[Tuple[int, str]] = [] + + exclude_pick_to = args.exclude # Getting commits information directory = args.directory if args.directory else "." @@ -250,18 +338,22 @@ if __name__ == "__main__": create_change_log(versions) # Sort commits - pyside2_commits = sort_dict(pyside2_commits) - shiboken2_commits = sort_dict(shiboken2_commits) + pyside6_commits = sort_dict(pyside6_commits) + shiboken6_commits = sort_dict(shiboken6_commits) + pyside6_changelogs = sort_changelog(pyside6_changelogs) + shiboken6_changelogs = sort_changelog(shiboken6_changelogs) # Generate message print(content_header.replace("@VERSION", args.release). replace("@TYPE", args.type)) - print('\n'.join(pyside2_changelogs)) - print(gen_list(pyside2_commits)) - if not pyside2_changelogs and not pyside2_commits: + for c in pyside6_changelogs: + print(c[1]) + print(gen_list(pyside6_commits)) + if not pyside6_changelogs and not pyside6_commits: print(" - No changes") print(shiboken_header) - print('\n'.join(shiboken2_changelogs)) - print(gen_list(shiboken2_commits)) - if not shiboken2_changelogs and not shiboken2_commits: + for c in shiboken6_changelogs: + print(c[1]) + print(gen_list(shiboken6_commits)) + if not shiboken6_changelogs and not shiboken6_commits: print(" - No changes") |