From 7121a49e4e833aec4c34280b4210da9f8f9681c6 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 27 Jun 2024 09:54:01 +0200 Subject: [PATCH] `generate_changelog.py`: divide into "fixed/added/changed" sections (#4712) Make each release a little bit easier to do --- scripts/generate_changelog.py | 146 ++++++++++++++++++++++------------ 1 file changed, 93 insertions(+), 53 deletions(-) diff --git a/scripts/generate_changelog.py b/scripts/generate_changelog.py index 6b0fc71e..a282ce62 100755 --- a/scripts/generate_changelog.py +++ b/scripts/generate_changelog.py @@ -12,6 +12,8 @@ import multiprocessing import os import re import sys + +from collections import defaultdict from datetime import date from dataclasses import dataclass from typing import Any, List, Optional @@ -23,15 +25,13 @@ from tqdm import tqdm OWNER = "emilk" REPO = "egui" INCLUDE_LABELS = False # It adds quite a bit of visual noise -OFFICIAL_DEVS = [ - "emilk", -] @dataclass class PrInfo: + pr_number: int gh_user_name: str - pr_title: str + title: str labels: List[str] @@ -85,7 +85,7 @@ def fetch_pr_info(pr_number: int) -> Optional[PrInfo]: if response.status_code == 200: labels = [label["name"] for label in json["labels"]] gh_user_name = json["user"]["login"] - return PrInfo(gh_user_name=gh_user_name, pr_title=json["title"], labels=labels) + return PrInfo(pr_number=pr_number, gh_user_name=gh_user_name, title=json["title"], labels=labels) else: print(f"ERROR {url}: {response.status_code} - {json['message']}") return None @@ -101,18 +101,83 @@ def get_commit_info(commit: Any) -> CommitInfo: return CommitInfo(hexsha=commit.hexsha, title=commit.summary, pr_number=None) +def pr_summary(pr: PrInfo, crate_name: Optional[str] = None) -> str: + title = pr.title + + if crate_name is not None: + # Remove crate name prefix (common in PR titles): + title = remove_prefix(title, f"[{crate_name}] ") + title = remove_prefix(title, f"{crate_name}: ") + title = remove_prefix(title, f"`{crate_name}`: ") + + # Upper-case first letter: + title = title[0].upper() + title[1:] + + # Remove trailing periods: + title = title.rstrip(".") + + summary = f"{title} [#{pr.pr_number}](https://github.com/{OWNER}/{REPO}/pull/{pr.pr_number})" + + if INCLUDE_LABELS and 0 < len(pr.labels): + summary += f" ({', '.join(pr.labels)})" + + summary += f" by [@{pr.gh_user_name}](https://github.com/{pr.gh_user_name})" + + return summary + + +def pr_info_section(prs: List[PrInfo], *, crate_name: str, heading: Optional[str] = None) -> str: + result = "" + if 0 < len(prs): + if heading is not None: + result += f"### {heading}\n" + for pr in prs: + result += f"* {pr_summary(pr, crate_name)}\n" + result += "\n" + return result + + +def changelog_from_prs(pr_infos: List[PrInfo], crate_name: str) -> str: + if len(pr_infos) == 0: + return "Nothing new" + + if len(pr_infos) <= 5: + # For small crates, or small releases + return pr_info_section(pr_infos, crate_name=crate_name) + + + fixed = [] + added = [] + rest = [] + for pr in pr_infos: + summary = pr_summary(pr, crate_name) + if "bug" in pr.labels: + fixed.append(pr) + elif summary.startswith("Add") or "feature" in pr.labels: + added.append(pr) + else: + rest.append(pr) + + result = "" + + result += pr_info_section(added, crate_name=crate_name, heading="⭐ Added") + result += pr_info_section(rest, crate_name=crate_name, heading="🔧 Changed") + result += pr_info_section(fixed, crate_name=crate_name, heading="🐛 Fixed") + + return result.rstrip() + + def remove_prefix(text, prefix): if text.startswith(prefix): return text[len(prefix) :] return text # or whatever -def print_section(crate: str, items: List[str]) -> None: - if 0 < len(items): - print(f"#### {crate}") - for line in items: - print(f"* {line}") - print() +def print_section(heading: str, content: str) -> None: + if content != "": + print(f"## {heading}") + print(content) + print() def changelog_filepath(crate: str) -> str: @@ -124,10 +189,9 @@ def changelog_filepath(crate: str) -> str: return os.path.normpath(file_path) -def add_to_changelog_file(crate: str, items: List[str], version: str) -> None: +def add_to_changelog_file(crate: str, content: str, version: str) -> None: insert_text = f"\n## {version} - {date.today()}\n" - for item in items: - insert_text += f"* {item}\n" + insert_text += content insert_text += "\n" file_path = changelog_filepath(crate) @@ -193,7 +257,7 @@ def main() -> None: ignore_labels = ["CI", "dependencies"] - sections = {} + crate_sections = defaultdict(list) unsorted_prs = [] unsorted_commits = [] @@ -209,66 +273,42 @@ def main() -> None: unsorted_commits.append(summary) else: if f"[#{pr_number}]" in all_changelogs: - print(f"Ignoring PR that is already in the changelog: #{pr_number}") + print(f"* Ignoring PR that is already in the changelog: [#{pr_number}](https://github.com/{OWNER}/{REPO}/pull/{pr_number})") continue - # We prefer the PR title if available - title = pr_info.pr_title if pr_info else title - labels = pr_info.labels if pr_info else [] + assert pr_info is not None - if "exclude from changelog" in labels: + if "exclude from changelog" in pr_info.labels: continue - if "typo" in labels: + if "typo" in pr_info.labels: # We get so many typo PRs. Let's not flood the changelog with them. continue - summary = f"{title} [#{pr_number}](https://github.com/{OWNER}/{REPO}/pull/{pr_number})" - - if INCLUDE_LABELS and 0 < len(labels): - summary += f" ({', '.join(labels)})" - - if pr_info is not None: - gh_user_name = pr_info.gh_user_name - if gh_user_name not in OFFICIAL_DEVS: - summary += f" (thanks [@{gh_user_name}](https://github.com/{gh_user_name})!)" - added = False for crate in crate_names: - if crate in labels: - sections.setdefault(crate, []).append(summary) + if crate in pr_info.labels: + crate_sections[crate].append(pr_info) added = True if not added: - if not any(label in labels for label in ignore_labels): - unsorted_prs.append(summary) - - # Clean up: - for crate in crate_names: - if crate in sections: - items = sections[crate] - for i in range(len(items)): - line = items[i] - line = remove_prefix(line, f"[{crate}] ") - line = remove_prefix(line, f"{crate}: ") - line = remove_prefix(line, f"`{crate}`: ") - line = line[0].upper() + line[1:] # Upper-case first letter - items[i] = line + if not any(label in pr_info.labels for label in ignore_labels): + unsorted_prs.append(pr_summary(pr_info)) print() print(f"Full diff at https://github.com/emilk/egui/compare/{args.commit_range}") print() for crate in crate_names: - if crate in sections: - items = sections[crate] - print_section(crate, items) - print_section("Unsorted PRs", unsorted_prs) - print_section("Unsorted commits", unsorted_commits) + if crate in crate_sections: + prs = crate_sections[crate] + print_section(crate, changelog_from_prs(prs, crate)) + print_section("Unsorted PRs", "\n".join([f"* {item}" for item in unsorted_prs])) + print_section("Unsorted commits", "\n".join([f"* {item}" for item in unsorted_commits])) if args.write: for crate in crate_names: - items = sections[crate] if crate in sections else ["Nothing new"] + items = changelog_from_prs(crate_sections[crate], crate) add_to_changelog_file(crate, items, args.version)