Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions Docs-Gen/commit-history.py
Original file line number Diff line number Diff line change
@@ -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 <carl.bialorucki@reactos.org>
#

import subprocess
import sys
import re
import unicodedata
from collections import defaultdict

MISC_TITLE = "Miscellaneous"
MAX_MSG_LENGTH = 100
skip_prefixes = []

class Commit:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might wanna look into dataclasses (https://docs.python.org/3/library/dataclasses.html), that has some nice things like automatic pretty-printing etc.

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")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't you use utf8 for some of our authors?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did try making it utf-8 at first, but pasting that into our wiki broke on all the extended characters.

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 <parent_branch>")
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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this work?
You might need global skip_prefixes somewhere in main

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does work. See this page for example, which was made using this script: https://reactos.org/wiki/0.4.16_Commit_History#BOOTDATA


# 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} <nowiki>{truncate_msg(commit.message)}</nowiki>] ({commit.author})")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might make sense to link to github instead?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer Github too, but I figured someone browsing our wiki is more likely to want to use our git mirror. Maybe I am wrong 😄


print()

if __name__ == "__main__":
main()
85 changes: 85 additions & 0 deletions Docs-Gen/resolved-issues.py
Original file line number Diff line number Diff line change
@@ -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 <carl.bialorucki@reactos.org>
#

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")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

utf8 is probably better fitting here as well

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 <version>")
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} <nowiki>{key}</nowiki>] {normalize_summary(summary)}")
print()

if __name__ == "__main__":
main()