Skip to content


Browse files Browse the repository at this point in the history
feat: improved generation of changelog
  • Loading branch information
ErikBjare committed Nov 18, 2020
1 parent dcd815a commit 7e6acf2
Showing 1 changed file with 139 additions and 19 deletions.
158 changes: 139 additions & 19 deletions scripts/ 100644 → 100755
@@ -1,43 +1,163 @@
from subprocess import run as _run, STDOUT, PIPE
Script that outputs a changelog for the repository in the current directory and its submodules.

import shlex
import re
from typing import Optional, Tuple, List
from subprocess import run as _run, STDOUT, PIPE
from dataclasses import dataclass

class CommitMsg:
type: str
subtype: str
msg: str

class Commit:
id: str
msg: str
repo: str

def msg_processed(self) -> str:
"""Generates links from commit and issue references (like 0c14d77, #123) to correct repo and such"""
s = self.msg
s = re.sub(
s = re.sub(
s = re.sub(
return s

def parse_type(self) -> Optional[Tuple[str, str]]:
match ="^(\w+)(\((.+)\))?:", self.msg)
if match:
type =
subtype =
if type in ["build", "ci", "fix", "feat"]:
return type, subtype
return None

def type(self) -> Optional[str]:
type, _ = self.parse_type() or (None, None)
return type

def subtype(self) -> Optional[str]:
_, subtype = self.parse_type() or (None, None)
return subtype

def type_str(self) -> str:
type, subtype = self.parse_type() or (None, None)
return f"{type}" + (f"({subtype})" if subtype else "")

def format(self) -> str:
commit_link = commit_linkify(, self.repo) if else ""

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

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

def run(cmd, cwd=".") -> str:
p = _run(shlex.split(cmd), stdout=PIPE, stderr=STDOUT, encoding="utf8", cwd=cwd)
if p.returncode != 0:
raise Exception
return p.stdout

def process_line(s: str, repo: str) -> str:
"""Generates links from commit and issue references (like 0c14d77, #123) to correct repo and such"""
s = re.sub(r"#([0-9]+)", rf"[#\1]({repo}/issues/\1)", s)
return s

def pr_linkify(prid: str, repo: str) -> str:
return f"[#{prid}]({repo}/pulls/{prid})"

def commit_linkify(commitid: str, repo: str) -> str:
return f"[`{commitid}`]({repo}/commit/{commitid})"

def build():
prev_release = run("git describe --tags --abbrev=0").strip()
summary_bundle = run(f"git log {prev_release}...master --oneline --decorate")
print("### activitywatch (bundle repo)")
def summary_repo(
path: str, commitrange: str, filter_types: List[str]
) -> Tuple[str, List[str]]:
dirname = run("bash -c 'basename $(pwd)'", cwd=path).strip()
out = f"## {dirname}"

feats = ""
fixes = ""
misc = ""

summary_bundle = run(f"git log {commitrange} --oneline --no-decorate", cwd=path)
for line in summary_bundle.split("\n"):
if line:
commit = line.split(" ")[0]
line = ' '.join(line.split(' ')[1:])
commit_link = commit_linkify(commit, 'activitywatch')
line = f" - {line} ({commit_link})"
print(process_line(line, "activitywatch"))
commit = Commit(
id=line.split(" ")[0], msg=" ".join(line.split(" ")[1:]), repo=dirname,

entry = f"\n - {commit.format()}"
if commit.type == "feat":
feats += entry
elif commit.type == "fix":
fixes += entry
elif commit.type not in filter_types:
misc += entry

for name, entries in (("✨ Features", feats), ("🐛 Fixes", fixes), ("🔨 Misc", misc)):
if entries:
if "Misc" in name:
header = f"\n\n<details><summary><b>{name}</b></summary>\n<p>\n\n"
header = f"\n\n#### {name}"
out += header
out += entries
if "Misc" in name:
out += "\n\n</p></details>"

submodules = []
output = run("git submodule foreach 'basename $(pwd)'")
for line in output.split("\n"):
if not line or line.startswith("Entering"):

return out, submodules

def build(filter_types=["build", "ci", "tests"]):
prev_release = run("git describe --tags --abbrev=0").strip()
output, submodules = summary_repo(
".", commitrange=f"{prev_release}...master", filter_types=filter_types

# TODO: Include subsubmodules (like aw-webui)
# TODO: Include commits from merges (exclude merge commits themselves?)
# TODO: Use specific order (aw-webui should be one of the first, for example)
summary_subrepos = run(f"git submodule summary {prev_release}")
for s in summary_subrepos.split("\n\n"):
lines = s.split("\n")
header = lines[0]
if header.strip():
_, name, commitrange, count = header.split(" ")
name = name.strip(".").strip("/")
print(f"\n### {name} {commitrange}")
commits = [process_line(" - " + l.strip(" ").strip(">").strip(" "), name) for l in lines[1:]]

output, submodules = summary_repo(
f"./{name}", commitrange, filter_types=filter_types

if __name__ == "__main__":
Expand Down

0 comments on commit 7e6acf2

Please sign in to comment.