forked from py-pdf/pypdf
-
Notifications
You must be signed in to change notification settings - Fork 0
/
make_changelog.py
219 lines (172 loc) · 5.3 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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
"""Internal tool to update the changelog."""
import subprocess
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import List
@dataclass(frozen=True)
class Change:
"""Capture the data of a git commit."""
commit_hash: str
prefix: str
message: str
def main(changelog_path: str) -> None:
"""
Create a changelog.
Args:
changelog_path: The location of the CHANGELOG file
"""
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(tz=timezone.utc)
header = f"Version {new_version}, {today:%Y-%m-%d}\n"
header = header + "-" * (len(header) - 1) + "\n"
url = f"https://github.com/py-pdf/pypdf/compare/{git_tag}...{new_version}"
trailer = f"\n[Full Changelog]({url})\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:
"""
Increase the patch version of the git tag by one.
Args:
git_tag: Old version tag
Returns:
The new version where the patch version is bumped.
"""
# 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:
"""
Read the changelog.
Args:
changelog_path: Path to the CHANGELOG file
Returns:
Data of the CHANGELOG
"""
with open(changelog_path) as fh:
changelog = fh.read()
return changelog
def write_changelog(new_changelog: str, changelog_path: str) -> None:
"""
Write the changelog.
Args:
new_changelog: Contents of the new CHANGELOG
changelog_path: Path where the CHANGELOG file is
"""
with open(changelog_path, "w") as fh:
fh.write(new_changelog)
def get_formatted_changes(git_tag: str) -> str:
"""
Format the changes done since the last tag.
Args:
git_tag: the reference tag
Returns:
Changes done since git_tag
"""
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() -> str:
"""
Get the git tag most recently created.
Returns:
Most recently created 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: str) -> List[Change]:
"""
Get all commits since the last tag.
Args:
git_tag: Reference tag from which the changes to the current commit are
fetched.
Returns:
List of all changes since git_tag.
"""
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: str) -> Change:
"""
Parse the first line of a git commit message.
Args:
line: The first line of a git commit message.
Returns:
The parsed Change object
Raises:
ValueError: The commit line is not well-structured
"""
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")