From 77c3ee4c81a0396f6802f655a22fb3a410fda7e6 Mon Sep 17 00:00:00 2001 From: "Carl J. Bialorucki" Date: Wed, 26 Aug 2026 15:29:43 -0500 Subject: [PATCH 1/2] [DOCS-GEN] Add commit history generator --- Docs-Gen/commit-history.py | 83 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 Docs-Gen/commit-history.py diff --git a/Docs-Gen/commit-history.py b/Docs-Gen/commit-history.py new file mode 100644 index 0000000..ef81652 --- /dev/null +++ b/Docs-Gen/commit-history.py @@ -0,0 +1,83 @@ +# +# PROJECT: ReactOS Release Document Scripts +# LICENSE: MIT (https://spdx.org/licenses/MIT) +# PURPOSE: Creates a commit history mediawiki document for a release +# COPYRIGHT: Copyright 2026 Carl Bialorucki +# + +import subprocess +import sys +import re +import unicodedata +from collections import defaultdict + +MISC_TITLE = "Miscellaneous" +MAX_MSG_LENGTH = 100 +skip_prefixes = [] + +class Commit: + def __init__(self, sha, author, message): + self.sha = sha + # Normalize and strip author, message + self.author = unicodedata.normalize("NFKD", author.strip()).encode("ascii", "ignore").decode("ascii") + self.message = unicodedata.normalize("NFKD", message.strip()).encode("ascii", "ignore").decode("ascii") + +def truncate_msg(s): + return s if len(s) <= MAX_MSG_LENGTH else s[:MAX_MSG_LENGTH - 3] + "..." + +def run(cmd): + return subprocess.run(cmd, text=True, capture_output=True, encoding="utf-8").stdout.strip() + +def parse_message_prefix(msg): + # Skip over some prefixes + for prefix in skip_prefixes: + if msg.startswith(f"[{prefix}]"): + msg = msg[msg.find("]") + 1:].lstrip() + + match = re.match(r"\[([^\]:]+)(?::([^\]]+))?\]", msg) + if match: + group = match.group(1).upper() + subgroup = match.group(2) or None + return group, subgroup + return MISC_TITLE, None + +def main(): + if len(sys.argv) < 2: + print("Usage: python commit-history.py ") + sys.exit(1) + + parent = sys.argv[1] + branch = run("git rev-parse --abbrev-ref HEAD") + raw_commits = run(f'git log {parent}..{branch} --pretty=format:"%h|%an|%s" --reverse').split("\n") + current_version = branch.split("releases/", 1)[1] + # Add current version as a skipped prefix + skip_prefixes.append(current_version) + + # groups[group][subgroup] = list of commits + groups = defaultdict(lambda: defaultdict(list)) + + for entry in raw_commits: + if not entry.strip(): + continue + + sha, author, message = entry.split("|", 2) + commit = Commit(sha, author, message) + group, subgroup = parse_message_prefix(commit.message) + groups[group][subgroup].append(commit) + + print(f"''Note: commits prefixed with the version number (i.e. [{current_version}]) are specific to this release. Backported commits from after the release was branched will also have this prefix.''\n") + # All properly formatted commits first, then misc + for group in sorted(groups.keys(), key=lambda g: (g == MISC_TITLE, g)): + print(f"== {group} ==") + # None subgroup first, then alphabetical + for subgroup in sorted(groups[group].keys(), key=lambda x: (x is not None, x or "")): + if subgroup is not None: + print(f"=== {subgroup} ===") + + for commit in groups[group][subgroup]: + print(f"* [https://git.reactos.org/?p=reactos.git;a=commit;h={commit.sha} {truncate_msg(commit.message)}] ({commit.author})") + + print() + +if __name__ == "__main__": + main() From 567d95efb93e5cd891bd7ead7cb91b660a99a031 Mon Sep 17 00:00:00 2001 From: "Carl J. Bialorucki" Date: Wed, 26 Aug 2026 15:30:00 -0500 Subject: [PATCH 2/2] [DOCS-GEN] Add resolved issues generator --- Docs-Gen/resolved-issues.py | 85 +++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 Docs-Gen/resolved-issues.py diff --git a/Docs-Gen/resolved-issues.py b/Docs-Gen/resolved-issues.py new file mode 100644 index 0000000..18969a9 --- /dev/null +++ b/Docs-Gen/resolved-issues.py @@ -0,0 +1,85 @@ +# +# PROJECT: ReactOS Release Document Scripts +# LICENSE: MIT (https://spdx.org/licenses/MIT) +# PURPOSE: Creates a resolved issues mediawiki document for a release +# COPYRIGHT: Copyright 2026 Carl Bialorucki +# + +import sys +import unicodedata +import requests + +JIRA_URL = "https://jira.reactos.org/rest/api/2/search" +MAX_SUMMARY_LENGTH = 100 + +def normalize_summary(s): + s = unicodedata.normalize("NFKD", s).encode("ascii", "ignore").decode("ascii") + return s if len(s) <= MAX_SUMMARY_LENGTH else s[:MAX_SUMMARY_LENGTH - 3] + "..." + +def fetch_tickets(version): + jql = f'fixVersion = "{version}"' + + # Largest valid maxResults value is 1000 + # See: https://developer.atlassian.com/cloud/jira/platform/change-notice-for-get-search-max-results + params = { + "jql": jql, + "maxResults": 1000, + "fields": "key,summary,issuetype" + } + + response = requests.get(JIRA_URL, params=params) + response.raise_for_status() + data = response.json() + return data["issues"] + +def main(): + if len(sys.argv) < 2: + print("Usage: python resolved-issues.py ") + sys.exit(1) + + version = sys.argv[1] + issues = fetch_tickets(version) + + groups = { + "Bug Fixes": [], + "Epics": [], + "New Features": [], + "Stories": [], + "Tasks": [], + "Improvements": [], + "Sub-tasks": [], + } + + for issue in issues: + itype = issue["fields"]["issuetype"]["name"] + + if itype == "Bug": + groups["Bug Fixes"].append(issue) + elif itype == "Epic": + groups["Epics"].append(issue) + elif itype == "New Feature": + groups["New Features"].append(issue) + elif itype == "Story": + groups["Stories"].append(issue) + elif itype == "Task": + groups["Tasks"].append(issue) + elif itype == "Improvement": + groups["Improvements"].append(issue) + elif itype == "Sub-task": + groups["Sub-tasks"].append(issue) + + for group_name, issues in groups.items(): + print(f"== {group_name} ==") + if not issues: + print("''No issues were resolved in this category.''") + print() + continue + + for issue in issues: + key = issue["key"] + summary = issue["fields"]["summary"] + print(f"* [https://jira.reactos.org/browse/{key} {key}] {normalize_summary(summary)}") + print() + +if __name__ == "__main__": + main()