Created
January 6, 2023 21:47
-
-
Save medecau/da827533390abcc32c91f11c4d5bb34d to your computer and use it in GitHub Desktop.
A CHANGELOG bootstrap script
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import argparse | |
import re | |
import subprocess | |
import shlex | |
import itertools | |
# this will be used for SUBJECT_CATEGORIES | |
def insensitive(pattern): | |
"""Return a case-insensitive regex pattern.""" | |
return re.compile(pattern, re.IGNORECASE | re.VERBOSE) | |
# map categories to regex patterns | |
# the order of the categories is the order in which they will be printed | |
# the "Other" category is a catch-all and should always be last | |
SUBJECT_CATEGORIES = { | |
# these are verbose regex patterns, they are easier to read and write | |
# the keyword is used to determine the category of the commit | |
"Security": insensitive( | |
r""" | |
.* # match anything before the keyword | |
(?: # match any of the following keywords | |
vuln | |
|vulnerabl(e|ity|ities) | |
|security | |
) | |
.* # match anything after the keyword | |
""" | |
), | |
"Removed": insensitive( | |
r""" | |
.* | |
(?: # match any of the following keywords | |
remove(?:|d|s) | |
|delete(?:|d|s) | |
|drop(?:|ped|s) | |
|revert(?:|ed|s) | |
|undo(?:|es) | |
|disable(?:|d|s) | |
) | |
.* | |
""" | |
), | |
"Added": insensitive( | |
r""" | |
.* | |
(?: # match any of the following keywords | |
add(?:|ed|s) | |
|new | |
|implement(?:|ed|s) | |
|create(?:|d|s) | |
|feat(?:|ure|ures) | |
) | |
.* | |
""" | |
), | |
"Changed": insensitive( | |
r""" | |
.* | |
(?: # match any of the following keywords | |
refactor(?:|ed|es|ing) | |
|update(?:|d|s) | |
|change(?:|d|s) | |
|modif(?:y|ied|ies) | |
) | |
.* | |
""" | |
), | |
"Deprecated": insensitive( | |
r""" | |
.* | |
(?: # match any of the following keywords | |
deprecate(?:|d|s) | |
) | |
.* | |
""" | |
), | |
"Fixed": insensitive( | |
r""" | |
.* | |
(?: # match any of the following keywords | |
fix(?:|ed|es) | |
) | |
.* | |
""" | |
), | |
"Tests": insensitive( | |
r""" | |
.* | |
(?: # match any of the following keywords | |
test(?:|ed|ing|s) | |
) | |
.* | |
""" | |
), | |
"Documentation": insensitive( | |
r""" | |
.* | |
(?: # match any of the following keywords | |
doc(?:|ument|s) | |
|readme | |
) | |
.* | |
""" | |
), | |
"Chore": insensitive( | |
r""" | |
.* | |
(?: # match any of the following keywords | |
chore(?:|s) | |
|bump(?:|s) | |
) | |
.* | |
""" | |
), | |
# catch-all category | |
"Other": re.compile(r".*"), | |
} | |
def run_command(command): | |
"""Run a command and return the output.""" | |
return subprocess.check_output(shlex.split(command), text=True).strip() | |
def get_commit_subject_lines(rev_interval): | |
"""Get the subject lines for a given revision interval.""" | |
return run_command(f"git log --format='%s' {rev_interval}").splitlines() | |
def get_revision_date(tag): | |
"""Get the date of a tag.""" | |
return run_command(f"git log -1 --format=%cd --date=short {tag}") | |
def sort_commits(commits): | |
"""Sort commits by category priority.""" | |
# get the categories in priority order | |
categories_by_priority = list(SUBJECT_CATEGORIES.keys()) | |
# sort the commits by category priority | |
return sorted( | |
commits, | |
key=lambda cat: categories_by_priority.index(classify_commit_subject(cat)), | |
) | |
def classify_commit_subject(subject): | |
"""Classify a commit subject into a category.""" | |
for category, regex in SUBJECT_CATEGORIES.items(): | |
if regex.match(subject): | |
return category | |
return "other" | |
def print_rev_changelog(rev, rev_interval): | |
"""Print the changelog for a given revision interval.""" | |
commits = sort_commits(get_commit_subject_lines(rev_interval)) | |
grouped_commits = itertools.groupby(commits, classify_commit_subject) | |
print(f"\n# {get_revision_date(rev)}") | |
for category, subjects in grouped_commits: | |
print(f"\n## {category}\n") | |
for subject in subjects: | |
print(f"- {subject}") | |
def get_tags(): | |
"""Get the tags in reverse chronological order.""" | |
return run_command("git tag --sort=creatordate").splitlines()[::-1] | |
def main(mode): | |
"""Get the changelog for the current repository.""" | |
tags = get_tags() | |
# get the changelog for a revision interval | |
if mode == "latest": | |
print_rev_changelog("HEAD", f"{tags[0]}..HEAD") | |
return | |
# mode is 'bootstrap' | |
# we won't do this because you can do it with scriv now | |
# # get the changelog for HEAD | |
# print_rev_changelog("HEAD", f"{tags[0]}..HEAD") | |
# get the changelog for the rest of the tags | |
for idx in range(len(tags) - 1): | |
t1, t2 = tags[idx], tags[idx + 1] | |
print_rev_changelog(t1, f"{t2}..{t1}") | |
# get the changelog for the oldest tag | |
print_rev_changelog(tags[-1], tags[-1]) | |
if __name__ == "__main__": | |
tags = get_tags() | |
# setup argparse parser | |
parser = argparse.ArgumentParser( | |
description="Get the changelog for the current repository." | |
) | |
# add mode argument | |
parser.add_argument( | |
"mode", | |
nargs="?", | |
default="latest", | |
choices=["latest", "bootstrap"], | |
help="The mode to run the script in.", | |
) | |
# parse the arguments | |
args = parser.parse_args() | |
main(args.mode) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment