`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 os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from collections import defaultdict
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, List, Optional
|
from typing import Any, List, Optional
|
||||||
|
|
@ -23,15 +25,13 @@ from tqdm import tqdm
|
||||||
OWNER = "emilk"
|
OWNER = "emilk"
|
||||||
REPO = "egui"
|
REPO = "egui"
|
||||||
INCLUDE_LABELS = False # It adds quite a bit of visual noise
|
INCLUDE_LABELS = False # It adds quite a bit of visual noise
|
||||||
OFFICIAL_DEVS = [
|
|
||||||
"emilk",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class PrInfo:
|
class PrInfo:
|
||||||
|
pr_number: int
|
||||||
gh_user_name: str
|
gh_user_name: str
|
||||||
pr_title: str
|
title: str
|
||||||
labels: List[str]
|
labels: List[str]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -85,7 +85,7 @@ def fetch_pr_info(pr_number: int) -> Optional[PrInfo]:
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
labels = [label["name"] for label in json["labels"]]
|
labels = [label["name"] for label in json["labels"]]
|
||||||
gh_user_name = json["user"]["login"]
|
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:
|
else:
|
||||||
print(f"ERROR {url}: {response.status_code} - {json['message']}")
|
print(f"ERROR {url}: {response.status_code} - {json['message']}")
|
||||||
return None
|
return None
|
||||||
|
|
@ -101,18 +101,83 @@ def get_commit_info(commit: Any) -> CommitInfo:
|
||||||
return CommitInfo(hexsha=commit.hexsha, title=commit.summary, pr_number=None)
|
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):
|
def remove_prefix(text, prefix):
|
||||||
if text.startswith(prefix):
|
if text.startswith(prefix):
|
||||||
return text[len(prefix) :]
|
return text[len(prefix) :]
|
||||||
return text # or whatever
|
return text # or whatever
|
||||||
|
|
||||||
|
|
||||||
def print_section(crate: str, items: List[str]) -> None:
|
def print_section(heading: str, content: str) -> None:
|
||||||
if 0 < len(items):
|
if content != "":
|
||||||
print(f"#### {crate}")
|
print(f"## {heading}")
|
||||||
for line in items:
|
print(content)
|
||||||
print(f"* {line}")
|
print()
|
||||||
print()
|
|
||||||
|
|
||||||
|
|
||||||
def changelog_filepath(crate: str) -> str:
|
def changelog_filepath(crate: str) -> str:
|
||||||
|
|
@ -124,10 +189,9 @@ def changelog_filepath(crate: str) -> str:
|
||||||
return os.path.normpath(file_path)
|
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"
|
insert_text = f"\n## {version} - {date.today()}\n"
|
||||||
for item in items:
|
insert_text += content
|
||||||
insert_text += f"* {item}\n"
|
|
||||||
insert_text += "\n"
|
insert_text += "\n"
|
||||||
|
|
||||||
file_path = changelog_filepath(crate)
|
file_path = changelog_filepath(crate)
|
||||||
|
|
@ -193,7 +257,7 @@ def main() -> None:
|
||||||
|
|
||||||
ignore_labels = ["CI", "dependencies"]
|
ignore_labels = ["CI", "dependencies"]
|
||||||
|
|
||||||
sections = {}
|
crate_sections = defaultdict(list)
|
||||||
unsorted_prs = []
|
unsorted_prs = []
|
||||||
unsorted_commits = []
|
unsorted_commits = []
|
||||||
|
|
||||||
|
|
@ -209,66 +273,42 @@ def main() -> None:
|
||||||
unsorted_commits.append(summary)
|
unsorted_commits.append(summary)
|
||||||
else:
|
else:
|
||||||
if f"[#{pr_number}]" in all_changelogs:
|
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
|
continue
|
||||||
|
|
||||||
# We prefer the PR title if available
|
assert pr_info is not None
|
||||||
title = pr_info.pr_title if pr_info else title
|
|
||||||
labels = pr_info.labels if pr_info else []
|
|
||||||
|
|
||||||
if "exclude from changelog" in labels:
|
if "exclude from changelog" in pr_info.labels:
|
||||||
continue
|
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.
|
# We get so many typo PRs. Let's not flood the changelog with them.
|
||||||
continue
|
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
|
added = False
|
||||||
|
|
||||||
for crate in crate_names:
|
for crate in crate_names:
|
||||||
if crate in labels:
|
if crate in pr_info.labels:
|
||||||
sections.setdefault(crate, []).append(summary)
|
crate_sections[crate].append(pr_info)
|
||||||
added = True
|
added = True
|
||||||
|
|
||||||
if not added:
|
if not added:
|
||||||
if not any(label in labels for label in ignore_labels):
|
if not any(label in pr_info.labels for label in ignore_labels):
|
||||||
unsorted_prs.append(summary)
|
unsorted_prs.append(pr_summary(pr_info))
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
|
|
||||||
print()
|
print()
|
||||||
print(f"Full diff at https://github.com/emilk/egui/compare/{args.commit_range}")
|
print(f"Full diff at https://github.com/emilk/egui/compare/{args.commit_range}")
|
||||||
print()
|
print()
|
||||||
for crate in crate_names:
|
for crate in crate_names:
|
||||||
if crate in sections:
|
if crate in crate_sections:
|
||||||
items = sections[crate]
|
prs = crate_sections[crate]
|
||||||
print_section(crate, items)
|
print_section(crate, changelog_from_prs(prs, crate))
|
||||||
print_section("Unsorted PRs", unsorted_prs)
|
print_section("Unsorted PRs", "\n".join([f"* {item}" for item in unsorted_prs]))
|
||||||
print_section("Unsorted commits", unsorted_commits)
|
print_section("Unsorted commits", "\n".join([f"* {item}" for item in unsorted_commits]))
|
||||||
|
|
||||||
if args.write:
|
if args.write:
|
||||||
for crate in crate_names:
|
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)
|
add_to_changelog_file(crate, items, args.version)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue