From 2e3eb1b7a894e7511648da4d500157c257407519 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Mon, 18 Apr 2022 10:55:44 +0200 Subject: [PATCH] DEV: Add tool to generate changelog --- make_changelog.py | 136 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 make_changelog.py diff --git a/make_changelog.py b/make_changelog.py new file mode 100644 index 000000000..de93c813e --- /dev/null +++ b/make_changelog.py @@ -0,0 +1,136 @@ +"""Internal tool to update the changelog.""" + +import subprocess +from dataclasses import dataclass +from datetime import datetime +from typing import List + + +@dataclass(frozen=True) +class Change: + commit_hash: str + prefix: str + message: str + + +def main(changelog_path: str): + changelog = get_changelog(changelog_path) + git_tag = get_most_recent_git_tag() + changes = get_formatted_changes(git_tag) + print("-" * 80) + print(changes) + # TODO: Write changes to changelog + new_version = version_bump(git_tag) + today = datetime.now() + header = f"Version {new_version}, {today:%Y-%m-%d}\n" + header = header + "-" * (len(header) - 1) + "\n" + trailer = f"All changes: https://github.com/py-pdf/PyPDF2/compare/{git_tag}...{new_version}" + new_entry = header + changes + trailer + print(new_entry) + + # TODO: Make idempotent - multiple calls to this script + # should not change the changelog + new_changelog = new_entry + changelog + write_changelog(new_changelog, changelog_path) + + +def version_bump(git_tag: str) -> str: + # just assume a patch version change + major, minor, patch = git_tag.split(".") + return f"{major}.{minor}.{int(patch) + 1}" + + +def get_changelog(changelog_path: str) -> str: + with open(changelog_path, "r") as fh: + changelog = fh.read() + return changelog + + +def write_changelog(new_changelog: str, changelog_path: str) -> None: + with open(changelog_path, "w") as fh: + fh.write(new_changelog) + + +def get_formatted_changes(git_tag: str) -> str: + commits = get_git_commits_since_tag(git_tag) + + # Group by prefix + grouped = {} + for commit in commits: + if commit.prefix not in grouped: + grouped[commit.prefix] = [] + grouped[commit.prefix].append({"msg": commit.message}) + + # Order prefixes + order = ["DEP", "ENH", "BUG", "ROB", "DOC", "DEV", "MAINT", "TST", "STY"] + abbrev2long = { + "DEP": "Deprecations", + "ENH": "New Features", + "BUG": "Bug Fixes", + "ROB": "Robustness", + "DOC": "Documentation", + "DEV": "Developer Experience", + "MAINT": "Maintenance", + "TST": "Testing", + "STY": "Code Style", + } + + # Create output + output = "" + for prefix in order: + if prefix not in grouped: + continue + output += f"\n{abbrev2long[prefix]} ({prefix}):\n" # header + for commit in grouped[prefix]: + output += f"- {commit['msg']}\n" + del grouped[prefix] + + if grouped: + print("@" * 80) + output += "\nYou forgot something!:\n" + for prefix in grouped: + output += f"- {prefix}: {grouped[prefix]}\n" + print("@" * 80) + + return output + + +def get_most_recent_git_tag(): + git_tag = str( + subprocess.check_output( + ["git", "describe", "--abbrev=0"], stderr=subprocess.STDOUT + ) + ).strip("'b\\n") + return git_tag + + +def get_git_commits_since_tag(git_tag) -> List[Change]: + commits = str( + subprocess.check_output( + ["git", "--no-pager", "log", f"{git_tag}..HEAD", "--oneline"], + stderr=subprocess.STDOUT, + ) + ).strip("'b\\n") + return [parse_commit_line(line) for line in commits.split("\\n")] + + +def parse_commit_line(line) -> Change: + commit_hash, rest = line[: len("d5a5eea")], line[len("d5a5eea") :] + if ":" in rest: + prefix, message = rest.split(":", 1) + else: + prefix = "" + message = rest + + # Standardize + message.strip() + + prefix = prefix.strip() + if prefix == "DOCS": + prefix = "DOC" + + return Change(commit_hash=commit_hash, prefix=prefix, message=message) + + +if __name__ == "__main__": + main("CHANGELOG")