|
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 |
2 | 7 | 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 "" |
3 | 71 |
|
| 72 | + return f"{self.msg_processed}" + (f" ({commit_link})" if commit_link else "") |
4 | 73 |
|
5 |
| -def run(cmd) -> str: |
6 |
| - return _run(cmd.split(" "), stdout=PIPE, stderr=STDOUT, encoding="utf8").stdout |
7 | 74 |
|
| 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 |
8 | 82 |
|
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})" |
13 | 86 |
|
14 | 87 |
|
15 | 88 | def commit_linkify(commitid: str, repo: str) -> str:
|
16 | 89 | return f"[`{commitid}`](https://github.com/ActivityWatch/{repo}/commit/{commitid})"
|
17 | 90 |
|
18 | 91 |
|
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) |
23 | 103 | for line in summary_bundle.split("\n"):
|
24 | 104 | 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>" |
30 | 127 |
|
| 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) |
31 | 148 | summary_subrepos = run(f"git submodule summary {prev_release}")
|
32 | 149 | for s in summary_subrepos.split("\n\n"):
|
33 | 150 | lines = s.split("\n")
|
34 | 151 | header = lines[0]
|
35 | 152 | if header.strip():
|
| 153 | + print("\n") |
36 | 154 | _, name, commitrange, count = header.split(" ")
|
37 | 155 | 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) |
41 | 161 |
|
42 | 162 |
|
43 | 163 | if __name__ == "__main__":
|
|
0 commit comments