forked from Flexget/Flexget
-
Notifications
You must be signed in to change notification settings - Fork 0
/
update-changelog.py
160 lines (143 loc) · 6.14 KB
/
update-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
import collections
import re
import sys
from typing import Generator, Iterable, List, Optional, Tuple
from git import Repo
class MDChangeSet:
"""Represents a markdown change-set for a single version."""
CATEGORIES = [
('### Added\n', ['add', 'added', 'feature']),
('### Changed\n', ['change', 'changed', 'update']),
('### Fixed\n', ['fix', 'fixed']),
('### Deprecated\n', ['deprecate', 'deprecated']),
('### Removed\n', ['remove', 'removed']),
]
def __init__(self) -> None:
self.pre_header = ['\n']
self.version_header = ''
self.post_header: List[str] = []
self.sections: collections.OrderedDict[str, List[str]] = collections.OrderedDict()
self.footer: List[str] = []
@classmethod
def from_md_lines(cls, lines):
"""Parse an existing markdown changeset section and return the VersionLog instance."""
instance = cls()
instance.pre_header, version_header, tail = isplit('## ', lines)
if version_header:
instance.version_header = version_header
instance.post_header, section, tail = isplit('### ', tail)
while section:
instance.sections[section], section, tail = isplit('### ', tail)
return instance
def parse_message(self, message: str) -> bool:
"""
Parses a git commit message and formats and adds any tagged messages to this changeset.
Returns True if one or more changelog messages was found.
"""
found = False
for cat, item in self.change_items(message):
found = True
item = re.sub(
r'#(\d{3,4})', r'[#\1](https://github.com/Flexget/Flexget/issues/\1)', item
)
item = f'- {item}\n'
self.sections.setdefault(cat, ['\n']).insert(0, item)
return found
def change_items(self, message: str):
"""An iterator of changelog updates from a commit message in the form (category, message)"""
for line in message.split('\n'):
for cat_match in re.finditer(r'\[(\w+)\]', line):
found_cat = self.cat_lookup(cat_match.group(1))
if found_cat:
line = line.replace(cat_match.group(0), '').strip()
yield found_cat, line
def cat_lookup(self, cat: str) -> Optional[str]:
"""Return an official category for `cat` tag text."""
for cat_item in self.CATEGORIES:
if cat.lower() in cat_item[1]:
return cat_item[0]
return None
def to_md_lines(self) -> Generator[str, None, None]:
"""An iterator over the markdown lines representing this changeset."""
yield from self.pre_header
yield self.version_header
yield from self.post_header
for section, items in self.sections.items():
yield section
yield from items
yield from self.footer
def isplit(
start_text: str, iterator: Iterable[str]
) -> Tuple[List[str], Optional[str], Iterable[str]]:
"""Returns head, match, tail tuple, where match is the first line that starts with `start_text`"""
head: List[str] = []
iterator = iter(iterator)
for item in iterator:
if item.startswith(start_text):
return head, item, iterator
head.append(item)
return head, None, iterator
if __name__ == '__main__':
try:
filename = sys.argv[1]
except IndexError:
print('No filename specified, using changelog.md')
filename = 'changelog.md'
with open(filename, encoding='utf-8') as logfile:
pre_lines, start_comment, tail = isplit('<!---', logfile)
active_lines, end_comment, tail = isplit('<!---', tail)
post_lines = list(tail)
repo = Repo('.')
cur_ver = MDChangeSet.from_md_lines(active_lines)
latestref = re.match(r'<!---\s*([\d\w]+)', start_comment).group(1)
oldestref = re.match(r'<!---\s*([\d\w]+)', end_comment).group(1)
released_vers: List[MDChangeSet] = []
commits = list(repo.iter_commits(f'{latestref}..HEAD', reverse=True))
modified = False
if commits:
tags = {}
for tag in repo.tags:
tags[tag.commit.hexsha] = tag
for commit in commits:
if cur_ver.parse_message(commit.message):
modified = True
if commit.hexsha in tags:
modified = True
# Tag changeset with release date and version and create new current changeset
tag_name = tags[commit.hexsha].name
version = tag_name.lstrip('v')
release_date = tags[commit.hexsha].commit.committed_datetime.strftime('%Y-%m-%d')
cur_ver.version_header = f'## {version} ({release_date})\n'
diffstartref = oldestref
if oldestref in tags:
diffstartref = tags[oldestref].name
cur_ver.post_header.insert(
0,
'[all commits]'
f'(https://github.com/Flexget/Flexget/compare/{diffstartref}...{tag_name})\n',
)
released_vers.insert(0, cur_ver)
cur_ver = MDChangeSet()
oldestref = commit.hexsha
verfile = repo.tree('HEAD')['flexget/_version.py'].data_stream.read()
__version__: Optional[str] = None
try:
exec(verfile) # pylint: disable=W0122
except Exception:
pass
new_version_header = f'## {__version__} (unreleased)\n'
if new_version_header != cur_ver.version_header:
cur_ver.version_header = new_version_header
modified = True
if modified:
print('Writing modified changelog.')
with open(filename, 'w', encoding='utf-8') as logfile:
logfile.writelines(pre_lines)
logfile.write(f'<!---{commit.hexsha}--->\n')
logfile.writelines(cur_ver.to_md_lines())
logfile.write(f'<!---{oldestref}--->\n')
for ver in released_vers:
logfile.writelines(ver.to_md_lines())
logfile.writelines(post_lines)
else:
print('No updates to write.')