InstantMesh / scripts /generate_changelog.py
Jeremy Leibs
Initial commit
344c16f unverified
#!/usr/bin/env python3
# Copied from https://github.com/rerun-io/rerun_template
"""
Summarizes recent PRs based on their GitHub labels.
The result can be copy-pasted into CHANGELOG.md,
though it often needs some manual editing too.
"""
from __future__ import annotations
import argparse
import multiprocessing
import os
import re
import sys
from dataclasses import dataclass
from typing import Any, Optional
import requests
from git import Repo # pip install GitPython
from tqdm import tqdm
OWNER = "rerun-io"
REPO = "new_repo_name"
INCLUDE_LABELS = False # It adds quite a bit of visual noise
OFFICIAL_RERUN_DEVS = [
"abey79",
"emilk",
"jleibs",
"jprochazk",
"nikolausWest",
"teh-cmc",
"Wumpf",
]
@dataclass
class PrInfo:
gh_user_name: str
pr_title: str
labels: list[str]
@dataclass
class CommitInfo:
hexsha: str
title: str
pr_number: Optional[int]
def get_github_token() -> str:
token = os.environ.get("GH_ACCESS_TOKEN", "")
if token != "":
return token
home_dir = os.path.expanduser("~")
token_file = os.path.join(home_dir, ".githubtoken")
try:
with open(token_file, encoding="utf8") as f:
token = f.read().strip()
return token
except Exception:
pass
print("ERROR: expected a GitHub token in the environment variable GH_ACCESS_TOKEN or in ~/.githubtoken")
sys.exit(1)
# Slow
def fetch_pr_info_from_commit_info(commit_info: CommitInfo) -> Optional[PrInfo]:
if commit_info.pr_number is None:
return None
else:
return fetch_pr_info(commit_info.pr_number)
# Slow
def fetch_pr_info(pr_number: int) -> Optional[PrInfo]:
url = f"https://api.github.com/repos/{OWNER}/{REPO}/pulls/{pr_number}"
gh_access_token = get_github_token()
headers = {"Authorization": f"Token {gh_access_token}"}
response = requests.get(url, headers=headers)
json = response.json()
# Check if the request was successful (status code 200)
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)
else:
print(f"ERROR {url}: {response.status_code} - {json['message']}")
return None
def get_commit_info(commit: Any) -> CommitInfo:
match = re.match(r"(.*) \(#(\d+)\)", commit.summary)
if match:
title = str(match.group(1))
pr_number = int(match.group(2))
return CommitInfo(hexsha=commit.hexsha, title=title, pr_number=pr_number)
else:
return CommitInfo(hexsha=commit.hexsha, title=commit.summary, pr_number=None)
def remove_prefix(text: str, prefix: str) -> str:
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 main() -> None:
parser = argparse.ArgumentParser(description="Generate a changelog.")
parser.add_argument("--commit-range", help="e.g. 0.1.0..HEAD", required=True)
args = parser.parse_args()
repo = Repo(".")
commits = list(repo.iter_commits(args.commit_range))
commits.reverse() # Most recent last
commit_infos = list(map(get_commit_info, commits))
pool = multiprocessing.Pool()
pr_infos = list(
tqdm(
pool.imap(fetch_pr_info_from_commit_info, commit_infos),
total=len(commit_infos),
desc="Fetch PR info commits",
)
)
prs = []
unsorted_commits = []
for commit_info, pr_info in zip(commit_infos, pr_infos):
hexsha = commit_info.hexsha
title = commit_info.title
title = title.rstrip(".").strip() # Some PR end with an unnecessary period
pr_number = commit_info.pr_number
if pr_number is None:
# Someone committed straight to main:
summary = f"{title} [{hexsha[:7]}](https://github.com/{OWNER}/{REPO}/commit/{hexsha})"
unsorted_commits.append(summary)
else:
# 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 []
if "exclude from changelog" in labels:
continue
if "typo" in 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_RERUN_DEVS:
summary += f" (thanks [@{gh_user_name}](https://github.com/{gh_user_name})!)"
prs.append(summary)
# Clean up:
for i in range(len(prs)):
line = prs[i]
line = line[0].upper() + line[1:] # Upper-case first letter
prs[i] = line
print()
print(f"Full diff at https://github.com/rerun-io/{REPO}/compare/{args.commit_range}")
print()
print_section("PRs", prs)
print_section("Unsorted commits", unsorted_commits)
if __name__ == "__main__":
main()