File: make_changelog.py

package info (click to toggle)
pypdf2 2.12.1-3%2Bdeb12u1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 27,144 kB
  • sloc: python: 28,767; makefile: 119; sh: 2
file content (151 lines) | stat: -rw-r--r-- 4,040 bytes parent folder | download
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
"""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:
    """Capture the data of a git commit."""

    commit_hash: str
    prefix: str
    message: str


def main(changelog_path: str):
    """Create a changelog."""
    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"\n[Full 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")