Skip to content

Commit 7e6acf2

Browse files
committed
feat: improved generation of changelog
1 parent dcd815a commit 7e6acf2

File tree

1 file changed

+139
-19
lines changed

1 file changed

+139
-19
lines changed

scripts/build_changelog.py

100644100755
+139-19
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,163 @@
1-
from subprocess import run as _run, STDOUT, PIPE
1+
#!/usr/bin/python3
2+
"""
3+
Script that outputs a changelog for the repository in the current directory and its submodules.
4+
"""
5+
6+
import shlex
27
import re
8+
from typing import Optional, Tuple, List
9+
from subprocess import run as _run, STDOUT, PIPE
10+
from dataclasses import dataclass
11+
12+
13+
class CommitMsg:
14+
type: str
15+
subtype: str
16+
msg: str
17+
18+
19+
@dataclass
20+
class Commit:
21+
id: str
22+
msg: str
23+
repo: str
24+
25+
@property
26+
def msg_processed(self) -> str:
27+
"""Generates links from commit and issue references (like 0c14d77, #123) to correct repo and such"""
28+
s = self.msg
29+
s = re.sub(
30+
r"[^(-]https://github.com/ActivityWatch/([\-\w\d]+)/(issues|pulls)/(\d+)",
31+
r"[#\3](https://github.com/ActivityWatch/\1/issues/\3)",
32+
s,
33+
)
34+
s = re.sub(
35+
r"#(\d+)",
36+
rf"[#\1](https://github.com/ActivityWatch/{self.repo}/issues/\1)",
37+
s,
38+
)
39+
s = re.sub(
40+
r"[\s\(][0-9a-f]{7}[\s\)]",
41+
rf"[`\0`](https://github.com/ActivityWatch/{self.repo}/issues/\0)",
42+
s,
43+
)
44+
return s
45+
46+
def parse_type(self) -> Optional[Tuple[str, str]]:
47+
match = re.search(r"^(\w+)(\((.+)\))?:", self.msg)
48+
if match:
49+
type = match.group(1)
50+
subtype = match.group(3)
51+
if type in ["build", "ci", "fix", "feat"]:
52+
return type, subtype
53+
return None
54+
55+
@property
56+
def type(self) -> Optional[str]:
57+
type, _ = self.parse_type() or (None, None)
58+
return type
59+
60+
@property
61+
def subtype(self) -> Optional[str]:
62+
_, subtype = self.parse_type() or (None, None)
63+
return subtype
64+
65+
def type_str(self) -> str:
66+
type, subtype = self.parse_type() or (None, None)
67+
return f"{type}" + (f"({subtype})" if subtype else "")
68+
69+
def format(self) -> str:
70+
commit_link = commit_linkify(self.id, self.repo) if self.id else ""
371

72+
return f"{self.msg_processed}" + (f" ({commit_link})" if commit_link else "")
473

5-
def run(cmd) -> str:
6-
return _run(cmd.split(" "), stdout=PIPE, stderr=STDOUT, encoding="utf8").stdout
774

75+
def run(cmd, cwd=".") -> str:
76+
p = _run(shlex.split(cmd), stdout=PIPE, stderr=STDOUT, encoding="utf8", cwd=cwd)
77+
if p.returncode != 0:
78+
print(p.stdout)
79+
print(p.stderr)
80+
raise Exception
81+
return p.stdout
882

9-
def process_line(s: str, repo: str) -> str:
10-
"""Generates links from commit and issue references (like 0c14d77, #123) to correct repo and such"""
11-
s = re.sub(r"#([0-9]+)", rf"[#\1](https://github.com/ActivityWatch/{repo}/issues/\1)", s)
12-
return s
83+
84+
def pr_linkify(prid: str, repo: str) -> str:
85+
return f"[#{prid}](https://github.com/ActivityWatch/{repo}/pulls/{prid})"
1386

1487

1588
def commit_linkify(commitid: str, repo: str) -> str:
1689
return f"[`{commitid}`](https://github.com/ActivityWatch/{repo}/commit/{commitid})"
1790

1891

19-
def build():
20-
prev_release = run("git describe --tags --abbrev=0").strip()
21-
summary_bundle = run(f"git log {prev_release}...master --oneline --decorate")
22-
print("### activitywatch (bundle repo)")
92+
def summary_repo(
93+
path: str, commitrange: str, filter_types: List[str]
94+
) -> Tuple[str, List[str]]:
95+
dirname = run("bash -c 'basename $(pwd)'", cwd=path).strip()
96+
out = f"## {dirname}"
97+
98+
feats = ""
99+
fixes = ""
100+
misc = ""
101+
102+
summary_bundle = run(f"git log {commitrange} --oneline --no-decorate", cwd=path)
23103
for line in summary_bundle.split("\n"):
24104
if line:
25-
commit = line.split(" ")[0]
26-
line = ' '.join(line.split(' ')[1:])
27-
commit_link = commit_linkify(commit, 'activitywatch')
28-
line = f" - {line} ({commit_link})"
29-
print(process_line(line, "activitywatch"))
105+
commit = Commit(
106+
id=line.split(" ")[0], msg=" ".join(line.split(" ")[1:]), repo=dirname,
107+
)
108+
109+
entry = f"\n - {commit.format()}"
110+
if commit.type == "feat":
111+
feats += entry
112+
elif commit.type == "fix":
113+
fixes += entry
114+
elif commit.type not in filter_types:
115+
misc += entry
116+
117+
for name, entries in (("✨ Features", feats), ("🐛 Fixes", fixes), ("🔨 Misc", misc)):
118+
if entries:
119+
if "Misc" in name:
120+
header = f"\n\n<details><summary><b>{name}</b></summary>\n<p>\n\n"
121+
else:
122+
header = f"\n\n#### {name}"
123+
out += header
124+
out += entries
125+
if "Misc" in name:
126+
out += "\n\n</p></details>"
30127

128+
submodules = []
129+
output = run("git submodule foreach 'basename $(pwd)'")
130+
for line in output.split("\n"):
131+
if not line or line.startswith("Entering"):
132+
continue
133+
submodules.append(line)
134+
135+
return out, submodules
136+
137+
138+
def build(filter_types=["build", "ci", "tests"]):
139+
prev_release = run("git describe --tags --abbrev=0").strip()
140+
output, submodules = summary_repo(
141+
".", commitrange=f"{prev_release}...master", filter_types=filter_types
142+
)
143+
print(output)
144+
145+
# TODO: Include subsubmodules (like aw-webui)
146+
# TODO: Include commits from merges (exclude merge commits themselves?)
147+
# TODO: Use specific order (aw-webui should be one of the first, for example)
31148
summary_subrepos = run(f"git submodule summary {prev_release}")
32149
for s in summary_subrepos.split("\n\n"):
33150
lines = s.split("\n")
34151
header = lines[0]
35152
if header.strip():
153+
print("\n")
36154
_, name, commitrange, count = header.split(" ")
37155
name = name.strip(".").strip("/")
38-
print(f"\n### {name} {commitrange}")
39-
commits = [process_line(" - " + l.strip(" ").strip(">").strip(" "), name) for l in lines[1:]]
40-
print("\n".join(commits))
156+
157+
output, submodules = summary_repo(
158+
f"./{name}", commitrange, filter_types=filter_types
159+
)
160+
print(output)
41161

42162

43163
if __name__ == "__main__":

0 commit comments

Comments
 (0)