-
Notifications
You must be signed in to change notification settings - Fork 84
Expand file tree
/
Copy pathcollect_changes.py
More file actions
executable file
·147 lines (121 loc) · 4.75 KB
/
collect_changes.py
File metadata and controls
executable file
·147 lines (121 loc) · 4.75 KB
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
#!/bin/env python3
# /// script
# requires-python = ">=3.13"
# dependencies = [
# "gitpython>=3.1.46,<3.2.0",
# "packaging>=26.0,<26.1",
# ]
# ///
# WARNING: DO NOT EDIT!
#
# This file was generated by plugin_template, and is managed by it. Please use
# './plugin-template --github pulp_python' to update this file.
#
# For more info visit https://github.com/pulp/plugin_template
import itertools
import json
import os
import re
import tomllib
import urllib.request
from pathlib import Path
from git import GitCommandError, Repo
from packaging.version import parse as parse_version
PYPI_PROJECT = "pulp_python"
# Read Towncrier settings
tc_settings = tomllib.loads(Path("pyproject.toml").read_text())["tool"]["towncrier"]
CHANGELOG_FILE = tc_settings.get("filename", "NEWS.rst")
START_STRING = tc_settings.get(
"start_string",
(
"<!-- towncrier release notes start -->\n"
if CHANGELOG_FILE.endswith(".md")
else ".. towncrier release notes start\n"
),
)
TITLE_FORMAT = tc_settings.get("title_format", "{name} {version} ({project_date})")
# Build a regex to find the header of a changelog section.
# It must have a single capture group to single out the version.
# see help(re.split) for more info.
NAME_REGEX = r".*"
VERSION_REGEX = r"[0-9]+\.[0-9]+\.[0-9][0-9ab]*"
VERSION_CAPTURE_REGEX = rf"(?:YANKED )?({VERSION_REGEX})"
DATE_REGEX = r"[0-9]{4}-[0-9]{2}-[0-9]{2}"
TITLE_REGEX = (
"("
+ re.escape(
TITLE_FORMAT.format(name="NAME_REGEX", version="VERSION_REGEX", project_date="DATE_REGEX")
)
.replace("NAME_REGEX", NAME_REGEX)
.replace("VERSION_REGEX", VERSION_CAPTURE_REGEX, 1)
.replace("VERSION_REGEX", VERSION_REGEX)
.replace("DATE_REGEX", DATE_REGEX)
+ ")"
)
def get_changelog(repo, branch):
return repo.git.show(f"{branch}:{CHANGELOG_FILE}") + "\n"
def _tokenize_changes(splits):
assert len(splits) % 3 == 0
for i in range(len(splits) // 3):
title = splits[3 * i]
version = parse_version(splits[3 * i + 1])
yield [version, title + splits[3 * i + 2]]
def split_changelog(changelog):
preamble, rest = changelog.split(START_STRING, maxsplit=1)
split_rest = re.split(TITLE_REGEX, rest)
return preamble + START_STRING + split_rest[0], list(_tokenize_changes(split_rest[1:]))
def main():
repo = Repo(os.getcwd())
remote = repo.remotes[0]
branches = [ref for ref in remote.refs if re.match(r"^([0-9]+)\.([0-9]+)$", ref.remote_head)]
branches.sort(key=lambda ref: parse_version(ref.remote_head), reverse=True)
branches = [ref.name for ref in branches]
changed = False
try:
response = urllib.request.urlopen(f"https://pypi.org/pypi/{PYPI_PROJECT}/json")
pypi_record = json.loads(response.read())
yanked_versions = {
parse_version(version): release[0]["yanked_reason"]
for version, release in pypi_record["releases"].items()
if release[0]["yanked"] is True
}
except Exception:
# If something failed, just don't mark anything as yanked.
yanked_versions = {}
with open(CHANGELOG_FILE, "r") as f:
main_changelog = f.read()
preamble, main_changes = split_changelog(main_changelog)
old_length = len(main_changes)
for branch in branches:
print(f"Looking for './{CHANGELOG_FILE}' at branch {branch}")
try:
changelog = get_changelog(repo, branch)
except GitCommandError:
print("No changelog found on this branch.")
continue
dummy, changes = split_changelog(changelog)
new_changes = sorted(main_changes + changes, key=lambda x: x[0], reverse=True)
# Now remove duplicates (retain the first one)
main_changes = [new_changes[0]]
for left, right in itertools.pairwise(new_changes):
if left[0] != right[0]:
main_changes.append(right)
if yanked_versions:
for change in main_changes:
if change[0] in yanked_versions and "YANKED" not in change[1].split("\n")[0]:
reason = yanked_versions[change[0]]
version = str(change[0])
change[1] = change[1].replace(version, "YANKED " + version, count=1)
if reason:
change[1] = change[1].replace("\n", f"\n\nYank reason: {reason}\n", count=1)
changed = True
new_length = len(main_changes)
if old_length < new_length or changed:
print(f"{new_length - old_length} new versions have been added (or something has changed).")
with open(CHANGELOG_FILE, "w") as fp:
fp.write(preamble)
for change in main_changes:
fp.write(change[1])
repo.git.commit("-m", "Update Changelog", CHANGELOG_FILE)
if __name__ == "__main__":
main()