aboutsummaryrefslogtreecommitdiffstats
path: root/tools/create_changelog.py
diff options
context:
space:
mode:
Diffstat (limited to 'tools/create_changelog.py')
-rw-r--r--tools/create_changelog.py248
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")