`generate_changelog.py`: divide into "fixed/added/changed" sections (#4712)
Make each release a little bit easier to do
This commit is contained in:
parent
5051e945e4
commit
7121a49e4e
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue