5
5
from __future__ import annotations
6
6
7
7
import re
8
+ import subprocess
8
9
from datetime import datetime , timezone
9
10
from pathlib import Path
10
11
from typing import Callable
14
15
15
16
BASE = Path (__file__ ).parent .parent .absolute ()
16
17
BEETS_INIT = BASE / "beets" / "__init__.py"
18
+ CHANGELOG = BASE / "docs" / "changelog.rst"
19
+
20
+ MD_CHANGELOG_SECTION_LIST = re .compile (r"- .+?(?=\n\n###|$)" , re .DOTALL )
21
+ version_header = r"\d+\.\d+\.\d+ \([^)]+\)"
22
+ RST_LATEST_CHANGES = re .compile (
23
+ rf"{ version_header } \n--+\s+(.+?)\n\n+{ version_header } " , re .DOTALL
24
+ )
17
25
18
26
19
27
def update_docs_config (text : str , new : Version ) -> str :
@@ -48,13 +56,10 @@ def update_changelog(text: str, new: Version) -> str:
48
56
r"(?<=__version__ = )[^\n]+" , f'"{ new } "' , text
49
57
),
50
58
),
51
- (BASE / "docs" / "changelog.rst" , update_changelog ),
59
+ (CHANGELOG , update_changelog ),
52
60
(BASE / "docs" / "conf.py" , update_docs_config ),
53
61
]
54
62
55
- GITHUB_USER = "beetbox"
56
- GITHUB_REPO = "beets"
57
-
58
63
59
64
def validate_new_version (
60
65
ctx : click .Context , param : click .Argument , value : Version
@@ -84,6 +89,54 @@ def bump_version(new: Version) -> None:
84
89
f .truncate ()
85
90
86
91
92
+ def rst2md (text : str ) -> str :
93
+ """Use Pandoc to convert text from ReST to Markdown."""
94
+ # Other backslashes with verbatim ranges.
95
+ rst = re .sub (r"(?<=[\s(])`([^`]+)`(?=[^_])" , r"``\1``" , text )
96
+
97
+ # Bug numbers.
98
+ rst = re .sub (r":bug:`(\d+)`" , r":bug: (#\1)" , rst )
99
+
100
+ # Users.
101
+ rst = re .sub (r":user:`(\w+)`" , r"@\1" , rst )
102
+ return (
103
+ subprocess .check_output (
104
+ ["/usr/bin/pandoc" , "--from=rst" , "--to=gfm" , "--wrap=none" ],
105
+ input = rst .encode (),
106
+ )
107
+ .decode ()
108
+ .strip ()
109
+ )
110
+
111
+
112
+ def changelog_as_markdown () -> str :
113
+ """Get the latest changelog entry as hacked up Markdown."""
114
+ with CHANGELOG .open () as f :
115
+ contents = f .read ()
116
+
117
+ m = RST_LATEST_CHANGES .search (contents )
118
+ rst = m .group (1 ) if m else ""
119
+
120
+ # Convert with Pandoc.
121
+ md = rst2md (rst )
122
+
123
+ # Make sections stand out
124
+ md = re .sub (r"^(\w.+?):$" , r"### \1" , md , flags = re .M )
125
+
126
+ # Highlight plugin names
127
+ md = re .sub (
128
+ r"^- `/?plugins/(\w+)`:?" , r"- Plugin **`\1`**:" , md , flags = re .M
129
+ )
130
+
131
+ # Highlights command names.
132
+ md = re .sub (r"^- `(\w+)-cmd`:?" , r"- Command **`\1`**:" , md , flags = re .M )
133
+
134
+ # sort list items alphabetically for each of the sections
135
+ return MD_CHANGELOG_SECTION_LIST .sub (
136
+ lambda m : "\n " .join (sorted (m .group ().splitlines ())), md
137
+ )
138
+
139
+
87
140
@click .group ()
88
141
def cli ():
89
142
pass
@@ -96,5 +149,11 @@ def bump(version: Version) -> None:
96
149
bump_version (version )
97
150
98
151
152
+ @cli .command ()
153
+ def changelog ():
154
+ """Get the most recent version's changelog as Markdown."""
155
+ print (changelog_as_markdown ())
156
+
157
+
99
158
if __name__ == "__main__" :
100
159
cli ()
0 commit comments