mirror of
https://github.com/Mudlet/Mudlet
synced 2026-08-13 18:26:27 -04:00
#### Brief overview of PR changes/additions Fix the in-app PTB changelog dialog which was showing garbled text (random commit hashes, file paths, broken formatting) by converting unsupported `<details>` HTML to plain markdown and fixing several changelog generation bugs. #### Motivation for adding to Mudlet The PTB changelog was unreadable for users - Qt's markdown renderer can't handle `<details>/<summary>` HTML tags, causing backtick content to leak out as random strings. Additionally, the markdown changelog builder was incorrectly HTML-escaping text, and empty changelog sections appeared when consecutive PTBs had no changes. #### Other info (issues closed, discussion etc)
287 lines
10 KiB
Lua
Executable file
287 lines
10 KiB
Lua
Executable file
local argparse = require "argparse"
|
|
local lunajson = require "lunajson"
|
|
|
|
-- don't load all of LuaGlobal, as that requires yajl installed
|
|
-- mock utf8 as we don't need it here either
|
|
utf8 = utf8 or {}
|
|
|
|
local github_workspace = os.getenv("GITHUB_WORKSPACE")
|
|
local function_list_path
|
|
if github_workspace then
|
|
-- the script struggles to load the load files relatively in CI
|
|
loadfile(github_workspace.. "/src/mudlet-lua/lua/StringUtils.lua")()
|
|
loadfile(github_workspace.."/src/mudlet-lua/lua/TableUtils.lua")()
|
|
function_list_path = github_workspace.."/src/lua-function-list.json"
|
|
else
|
|
loadfile("../src/mudlet-lua/lua/StringUtils.lua")()
|
|
loadfile("../src/mudlet-lua/lua/TableUtils.lua")()
|
|
function_list_path = "../src/lua-function-list.json"
|
|
end
|
|
|
|
local parser = argparse("generate-changelog.lua", "Generate a changelog from the HEAD until the most recent published commit.")
|
|
-- see https://argparse.readthedocs.io/en/stable/index.html
|
|
parser:option("-m --mode", 'mode to run in'):choices({"ptb", "release"}):count("1")
|
|
parser:option("-r --releasefile", "downloaded DBLSQD release feed file")
|
|
parser:option("-s --start-commit", "start commit to generate changelog from")
|
|
parser:option("-e --end-commit", "end commit to generate changelog to")
|
|
parser:option("-f --format", "output format", "html"):choices({"html", "md"}):count(1)
|
|
local args = parser:parse()
|
|
|
|
if (args.mode == "ptb" and not args.releasefile) then
|
|
error("-r or --releasefile is required for ptb mode")
|
|
elseif (args.mode == "release" and not (args.start_commit and args.end_commit)) then
|
|
error("--start-commit and --end-commit are required for release mode")
|
|
end
|
|
|
|
local MAX_COMMITS_PER_CHANGELOG = 100
|
|
|
|
|
|
function escape_for_html(text)
|
|
local escapes = {
|
|
["&"] = "&",
|
|
["<"] = "<",
|
|
[">"] = ">",
|
|
['"'] = """,
|
|
["'"] = "'"
|
|
}
|
|
|
|
return text:gsub(".", escapes)
|
|
end
|
|
|
|
local htmlBuilder = {
|
|
headerStart = [[<h5 style='margin-top: 1em;margin-bottom: 1em;'>]],
|
|
headerEnd = "</h5>",
|
|
converter = function(text)
|
|
local t = {}
|
|
text = text.."\n"
|
|
for s in string.gmatch(text, "(.-)\n") do
|
|
s = escape_for_html(s)
|
|
s = s:gsub("%(#(.-)%)", [[<a href='https://github.com/Mudlet/Mudlet/pull/%1'>(#%1)</a>]])
|
|
s = link_functions_html(s)
|
|
t[#t+1] = string.format("<p>%s</p>", s)
|
|
end
|
|
|
|
return table.concat(t, "\n")
|
|
end
|
|
}
|
|
|
|
local mdBuilder = {
|
|
headerStart = "##### ",
|
|
headerEnd = "",
|
|
converter = function(text)
|
|
local t = {}
|
|
text = text.."\n"
|
|
for s in string.gmatch(text, "(.-)\n") do
|
|
s = s:gsub("%(#(.-)%)", "[#%1](https://github.com/Mudlet/Mudlet/pull/%1)")
|
|
s = link_functions_md(s)
|
|
t[#t+1] = string.format("\\%s\n", s)
|
|
end
|
|
|
|
return table.concat(t, "\n")
|
|
end
|
|
}
|
|
|
|
local builder = args.format == "md" and mdBuilder or htmlBuilder
|
|
|
|
-- Basic algorithm for the PTB mode is as follows:
|
|
-- retrieve last MAX_COMMITS_PER_CHANGELOG commit hashes from current branch
|
|
-- retrieve list of PTB releases and the hashes at the end of them (ie 4.14.1-ptb-2022-01-29-e8084)
|
|
-- go through the list of commits, collecting hashes not present in releases
|
|
-- then get the changelog for the range of hashes
|
|
-- then sort the changelog into categories
|
|
-- html-ize and print the results.
|
|
|
|
-- credit: https://stackoverflow.com/a/326715/72944
|
|
function os.capture(cmd, raw)
|
|
local f = assert(io.popen(cmd, 'r'))
|
|
local s = assert(f:read('*a'))
|
|
f:close()
|
|
if raw then return s end
|
|
s = string.gsub(s, '^%s+', '')
|
|
s = string.gsub(s, '%s+$', '')
|
|
s = string.gsub(s, '[\n\r]+', ' ')
|
|
return s
|
|
end
|
|
|
|
function read_file(path)
|
|
local file = io.open(path, "rb")
|
|
if not file then return nil end
|
|
local content = file:read "*a"
|
|
file:close()
|
|
return content
|
|
end
|
|
|
|
-- Load function names from lua-function-list.json for auto-linking
|
|
local function_names = {}
|
|
local function_list_content = read_file(function_list_path)
|
|
if function_list_content then
|
|
local function_list = lunajson.decode(function_list_content)
|
|
for name, _ in pairs(function_list) do
|
|
function_names[#function_names + 1] = name
|
|
end
|
|
-- Sort by length descending so longer names are matched first
|
|
-- (e.g., "selectString" before "select")
|
|
table.sort(function_names, function(a, b) return #a > #b end)
|
|
end
|
|
|
|
-- Escape special Lua pattern characters
|
|
local function escape_pattern(str)
|
|
return str:gsub("([%(%)%.%%%+%-%*%?%[%]%^%$])", "%%%1")
|
|
end
|
|
|
|
-- Link function names to wiki documentation (HTML format)
|
|
function link_functions_html(text)
|
|
for _, name in ipairs(function_names) do
|
|
local escaped_name = escape_pattern(name)
|
|
-- Match function name with optional () at word boundaries
|
|
-- Use frontier pattern %f for word boundary detection on both sides
|
|
local pattern = "(%f[%w])" .. escaped_name .. "(%f[^%w])(%(?)(%)?)"
|
|
local replacement = "%1<a href='https://wiki.mudlet.org/w/Manual:Lua_Functions#" .. name .. "'>" .. name .. "</a>%3%4"
|
|
text = text:gsub(pattern, replacement)
|
|
end
|
|
return text
|
|
end
|
|
|
|
-- Link function names to wiki documentation (Markdown format)
|
|
function link_functions_md(text)
|
|
for _, name in ipairs(function_names) do
|
|
local escaped_name = escape_pattern(name)
|
|
-- Match function name with optional () at word boundaries
|
|
local pattern = "(%f[%w])" .. escaped_name .. "(%f[^%w])(%(?)(%)?)"
|
|
local replacement = "%1[" .. name .. "](https://wiki.mudlet.org/w/Manual:Lua_Functions#" .. name .. ")%3%4"
|
|
text = text:gsub(pattern, replacement)
|
|
end
|
|
return text
|
|
end
|
|
|
|
function extract_released_sha1s(input)
|
|
local decoded = lunajson.decode(input)
|
|
|
|
local t = {}
|
|
for _, release in ipairs(decoded.releases) do
|
|
t[#t+1] = release.version:match(".+%-(.-)$")
|
|
end
|
|
|
|
return t
|
|
end
|
|
|
|
function extract_historical_sha1s()
|
|
local history, command
|
|
if github_workspace then
|
|
command = string.format("git log --pretty=%%H -n %d %s", MAX_COMMITS_PER_CHANGELOG, os.getenv("GITHUB_SHA"))
|
|
history = string.split(os.capture(command))
|
|
else
|
|
command = "git log --pretty=%H -n "..MAX_COMMITS_PER_CHANGELOG
|
|
history = string.split(os.capture(command))
|
|
end
|
|
|
|
local t = {}
|
|
for _, sha1 in ipairs(history) do
|
|
t[#t+1] = sha1:match("^(........)")
|
|
end
|
|
|
|
return t
|
|
end
|
|
|
|
function get_releases(location)
|
|
return read_file(location)
|
|
end
|
|
|
|
-- returns a list of commits that have been added since the last release
|
|
function scan_commits(historical_commits, released_commits)
|
|
local commits_added_since = {}
|
|
|
|
local released_commits_length = #released_commits[1]
|
|
for i, commit in ipairs(historical_commits) do historical_commits[i] = string.cut(commit, released_commits_length) end
|
|
|
|
for i, v in ipairs(historical_commits) do
|
|
if table.contains(released_commits, v) then
|
|
commits_added_since[#commits_added_since + 1] = v
|
|
return commits_added_since
|
|
end
|
|
commits_added_since[#commits_added_since + 1] = v
|
|
end
|
|
|
|
print("(hit the "..MAX_COMMITS_PER_CHANGELOG.." commit limit - couldn't find the latest published PTB release)")
|
|
return {}
|
|
end
|
|
|
|
function get_changelog(commit1, commit2)
|
|
return os.capture(string.format("git log --pretty=%%s %s..%s", commit1, commit2), true):trim()
|
|
end
|
|
|
|
function create_category_pattern(category)
|
|
if not category then return end
|
|
local nocase = category:genNocasePattern()
|
|
local res = f[[^{nocase}%w*:*%s*(.*)]]
|
|
return res
|
|
end
|
|
|
|
function print_sorted_changelog(changelog)
|
|
local chgtbl = changelog:split("\n")
|
|
local add, improve, fix, infra, other = {}, {}, {}, {}, {}
|
|
local prefix = "- "
|
|
for _,line in ipairs(chgtbl) do
|
|
local trimmedLine
|
|
local addPattern = create_category_pattern("add")
|
|
local improvePattern = create_category_pattern("improve")
|
|
local fixPattern = create_category_pattern("fix")
|
|
local infraPattern = create_category_pattern("infra")
|
|
local autoPattern = "^%(autocommit%) (.*)"
|
|
if line:match(addPattern) then
|
|
trimmedLine = prefix .. line:match(addPattern)
|
|
add[#add+1] = trimmedLine
|
|
elseif line:match(improvePattern) then
|
|
trimmedLine = prefix .. line:match(improvePattern)
|
|
improve[#improve+1] = trimmedLine
|
|
elseif line:match(fixPattern) then
|
|
trimmedLine = prefix .. line:match(fixPattern)
|
|
fix[#fix+1] = trimmedLine
|
|
elseif line:match(infraPattern) then
|
|
trimmedLine = prefix .. line:match(infraPattern)
|
|
infra[#infra+1] = trimmedLine
|
|
elseif line:match(autoPattern) then --"(autocommit)" to catch bot PRs which may not start with "infra"
|
|
trimmedLine = prefix .. line:match(autoPattern)
|
|
infra[#infra+1] = trimmedLine
|
|
elseif line:trim() ~= "" then
|
|
other[#other+1] = prefix .. line
|
|
end
|
|
end
|
|
local hopen = builder.headerStart
|
|
local hclose = builder.headerEnd
|
|
local addLines = lines_to_output(table.concat(add, "\n"))
|
|
addLines = addLines and f"{hopen}Added:{hclose}\n{addLines}\n" or ""
|
|
local improveLines = lines_to_output(table.concat(improve, "\n"))
|
|
improveLines = improveLines and f"{hopen}Improved:{hclose}\n{improveLines}\n" or ""
|
|
local fixLines = lines_to_output(table.concat(fix, "\n"))
|
|
fixLines = fixLines and f"{hopen}Fixed:{hclose}\n{fixLines}\n" or ""
|
|
local infraLines = lines_to_output(table.concat(infra, "\n"))
|
|
infraLines = infraLines and f"{hopen}Infrastructure:{hclose}\n{infraLines}\n" or ""
|
|
local otherLines = lines_to_output(table.concat(other, "\n"))
|
|
otherLines = otherLines and f"{hopen}Other:{hclose}\n{otherLines}\n" or ""
|
|
local final_changelog = f"{addLines}{improveLines}{fixLines}{infraLines}{otherLines}"
|
|
print(final_changelog)
|
|
end
|
|
|
|
function lines_to_output(lines)
|
|
if lines == "" then
|
|
return nil
|
|
end
|
|
return builder.converter(lines)
|
|
end
|
|
|
|
local start_commit, end_commit
|
|
if (args.mode == "ptb") then
|
|
local historical_commits = extract_historical_sha1s()
|
|
local released_commits = extract_released_sha1s(get_releases(args.releasefile))
|
|
local unpublished_commits = scan_commits(historical_commits, released_commits)
|
|
|
|
if table.is_empty(unpublished_commits) then print("(changelog couldn't be generated)") os.exit() end
|
|
start_commit, end_commit = unpublished_commits[#unpublished_commits], unpublished_commits[1]
|
|
else
|
|
start_commit, end_commit = args.start_commit, args.end_commit
|
|
end
|
|
|
|
local changelog = get_changelog(start_commit, end_commit)
|
|
|
|
print_sorted_changelog(changelog)
|