forked from py-pdf/pypdf
-
Notifications
You must be signed in to change notification settings - Fork 0
/
make_changelog.py
148 lines (116 loc) · 3.87 KB
/
make_changelog.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
"""Internal tool to update the changelog."""
import subprocess
from dataclasses import dataclass
from datetime import datetime
from typing import List
@dataclass(frozen=True)
class Change:
commit_hash: str
prefix: str
message: str
def main(changelog_path: str):
changelog = get_changelog(changelog_path)
git_tag = get_most_recent_git_tag()
changes = get_formatted_changes(git_tag)
print("-" * 80)
print(changes)
new_version = version_bump(git_tag)
today = datetime.now()
header = f"Version {new_version}, {today:%Y-%m-%d}\n"
header = header + "-" * (len(header) - 1) + "\n"
trailer = f"\nFull Changelog: https://github.com/py-pdf/PyPDF2/compare/{git_tag}...{new_version}\n\n"
new_entry = header + changes + trailer
print(new_entry)
# TODO: Make idempotent - multiple calls to this script
# should not change the changelog
new_changelog = new_entry + changelog
write_changelog(new_changelog, changelog_path)
def version_bump(git_tag: str) -> str:
# just assume a patch version change
major, minor, patch = git_tag.split(".")
return f"{major}.{minor}.{int(patch) + 1}"
def get_changelog(changelog_path: str) -> str:
with open(changelog_path) as fh:
changelog = fh.read()
return changelog
def write_changelog(new_changelog: str, changelog_path: str) -> None:
with open(changelog_path, "w") as fh:
fh.write(new_changelog)
def get_formatted_changes(git_tag: str) -> str:
commits = get_git_commits_since_tag(git_tag)
# Group by prefix
grouped = {}
for commit in commits:
if commit.prefix not in grouped:
grouped[commit.prefix] = []
grouped[commit.prefix].append({"msg": commit.message})
# Order prefixes
order = ["DEP", "ENH", "PI", "BUG", "ROB", "DOC", "DEV", "MAINT", "TST", "STY"]
abbrev2long = {
"DEP": "Deprecations",
"ENH": "New Features",
"BUG": "Bug Fixes",
"ROB": "Robustness",
"DOC": "Documentation",
"DEV": "Developer Experience",
"MAINT": "Maintenance",
"TST": "Testing",
"STY": "Code Style",
"PI": "Performance Improvements",
}
# Create output
output = ""
for prefix in order:
if prefix not in grouped:
continue
output += f"\n{abbrev2long[prefix]} ({prefix}):\n" # header
for commit in grouped[prefix]:
output += f"- {commit['msg']}\n"
del grouped[prefix]
if grouped:
print("@" * 80)
output += "\nYou forgot something!:\n"
for prefix in grouped:
output += f"- {prefix}: {grouped[prefix]}\n"
print("@" * 80)
return output
def get_most_recent_git_tag():
git_tag = str(
subprocess.check_output(
["git", "describe", "--abbrev=0"], stderr=subprocess.STDOUT
)
).strip("'b\\n")
return git_tag
def get_git_commits_since_tag(git_tag) -> List[Change]:
commits = str(
subprocess.check_output(
[
"git",
"--no-pager",
"log",
f"{git_tag}..HEAD",
'--pretty=format:"%h%x09%s"',
],
stderr=subprocess.STDOUT,
)
).strip("'b\\n")
return [parse_commit_line(line) for line in commits.split("\\n")]
def parse_commit_line(line) -> Change:
if "\\t" not in line:
raise ValueError(f"Invalid commit line: {line}")
commit_hash, rest = line.split("\\t", 1)
if ":" in rest:
prefix, message = rest.split(":", 1)
else:
prefix = ""
message = rest
# Standardize
message.strip()
if message.endswith('"'):
message = message[:-1]
prefix = prefix.strip()
if prefix == "DOCS":
prefix = "DOC"
return Change(commit_hash=commit_hash, prefix=prefix, message=message)
if __name__ == "__main__":
main("CHANGELOG.md")