diff options
context:
space:
mode:
authorBryce Kwon <bryce@brycekwon.com>
committerBryce Kwon <bryce@brycekwon.com>
commit
parent
tree
download
Rework link-commits into a rules filter
Diffstat (limited to 'extensions')
-rw-r--r--extensions/link-commits.lua154
1 file changed, 140 insertions, 14 deletions
diff --git a/extensions/link-commits.lua b/extensions/link-commits.lua
index 160a12d..e7b17cc 100644
--- a/extensions/link-commits.lua
+++ b/extensions/link-commits.lua
@@ -1,27 +1,153 @@
--- Turns object hashes and issue references in commit messages into links.
--- Use it with the commit-filter or repo.commit-filter setting and the lua:
--- prefix. cgit hands the filter the message already HTML-escaped, so this
--- only wraps matches in anchors.
+-- cgit commit-filter that turns git object names and configurable text
+-- references in commit messages into links. Use it with the commit-filter or
+-- repo.commit-filter setting and the lua: prefix.
+--
+-- commit-filter=lua:/path/to/link-commits.lua
+--
+-- cgit hands the filter the message already HTML-escaped, so this only wraps
+-- matches in anchors. No external dependencies. Runs on Lua 5.1 through 5.4
+-- and LuaJIT.
+--
+-- Two kinds of thing are linked, object names (runs of hex that look like git
+-- hashes) and any number of text-reference rules you define, each a pattern
+-- and a URL. Both are configured in the block below. All matches are resolved
+-- in a single left-to-right pass, so nothing is ever linked twice.
-local buffer
+--
+-- ===== CONFIGURATION =====
+--
+
+-- Object names (git hashes). Handled specially, because the length rule cannot
+-- be written as a plain Lua pattern.
+--
+-- Recognition is by shape, since a commit-filter cannot ask the repository
+-- whether a hash is real. Any hex run within the length bounds is linked,
+-- whatever mix of digits and letters it has, so abbreviated and all-digit
+-- hashes are both caught. The cost is that a long hex-looking number can now
+-- and then link to an object that does not exist, which cgit renders as a
+-- harmless "bad object name" page. Shape matching is inherently approximate,
+-- the length bounds are the only filter.
+local objects = {
+ -- Set false to stop linking bare hashes.
+ enabled = true,
+ -- A hex run within these lengths is linked. Git abbreviations run about 7
+ -- to 12 characters, full names are 40 (sha1) or 64 (sha256).
+ min_length = 7,
+ max_length = 64,
+ -- Link target, %s is replaced with the matched hash. "./?id=%s" is relative
+ -- to the current page and works for the common virtual-root layout.
+ url = "./?id=%s",
+}
+
+-- Text-reference rules. Each rule is a Lua pattern with ONE capture and a URL
+-- where %s is replaced by that capture, percent-encoded. The whole match is
+-- shown, the capture is what goes in the URL. Rules are tried in order and the
+-- leftmost match on the line wins, so put more specific patterns first. Leave
+-- the list empty to link only object names.
+--
+-- Lua patterns are not regular expressions. There is no alternation and no
+-- {n,m} repetition. %d is a digit, %a a letter, %w a letter or digit, %x a hex
+-- digit, and a literal magic character is escaped with %, so a literal '-' is
+-- '%-'. Reference: https://www.lua.org/manual/5.1/manual.html#5.4.1
+local rules = {
+ { pattern = "#(%d+)", url = "https://bugs.example.com/?bug=%s" },
+ -- { pattern = "CVE%-(%d%d%d%d%-%d+)", url = "https://www.cve.org/CVERecord?id=CVE-%s" },
+ -- { pattern = "!(%d+)", url = "https://gitlab.example.com/group/repo/-/merge_requests/%s" },
+ -- { pattern = "RFC%s?(%d+)", url = "https://www.rfc-editor.org/rfc/rfc%s" },
+}
+
+--
+-- =========================
+--
+
+local chunks = {}
+
+-- Percent-encode everything but the URL-unreserved characters, so a captured
+-- value cannot break out of the href attribute or the URL.
+local function url_encode(s)
+ return (string.gsub(s, "[^%w._~-]", function(c)
+ return string.format("%%%02X", string.byte(c))
+ end))
+end
+
+-- Build one anchor. url_template has %s where the encoded capture goes, display
+-- is the text shown. A function replacement is used so a '%' in the encoded
+-- value is not treated as a gsub reference.
+local function make_link(url_template, capture, display)
+ local encoded = url_encode(capture)
+ local href = string.gsub(url_template, "%%s", function() return encoded end)
+ return '<a href="' .. href .. '">' .. display .. '</a>'
+end
+
+-- Collect every candidate match as {s, e, pri, link}. A lower pri wins a tie on
+-- the same start position.
+local function collect(text)
+ local cands = {}
+ for pri, rule in ipairs(rules) do
+ -- A malformed pattern is an operator error, skip that rule rather than
+ -- failing the whole page.
+ pcall(function()
+ local init = 1
+ while init <= #text do
+ local s, e, cap = string.find(text, rule.pattern, init)
+ if not s then break end
+ if cap == nil then
+ cap = string.sub(text, s, e)
+ end
+ cands[#cands + 1] = {
+ s = s, e = e, pri = pri,
+ link = make_link(rule.url, cap, string.sub(text, s, e)),
+ }
+ init = (e >= s) and e + 1 or s + 1
+ end
+ end)
+ end
+ if objects.enabled then
+ local objpri = #rules + 1
+ local init = 1
+ while init <= #text do
+ local s, e, run = string.find(text, "%f[%w](%x+)%f[%W]", init)
+ if not s then break end
+ if #run >= objects.min_length and #run <= objects.max_length then
+ cands[#cands + 1] = {
+ s = s, e = e, pri = objpri,
+ link = make_link(objects.url, run, run),
+ }
+ end
+ init = e + 1
+ end
+ end
+ return cands
+end
function filter_open(...)
- buffer = ""
+ chunks = {}
end
function filter_write(str)
- buffer = buffer .. str
+ chunks[#chunks + 1] = str
end
function filter_close()
- -- Link a 7 to 64 character object name to this repository.
- local out = buffer:gsub("%f[%w](%x+)%f[%W]", function(hex)
- if #hex >= 7 and #hex <= 64 then
- return '<a href="./?id=' .. hex .. '">' .. hex .. '</a>'
+ local text = table.concat(chunks)
+ local cands = collect(text)
+ table.sort(cands, function(a, b)
+ if a.s ~= b.s then
+ return a.s < b.s
end
+ return a.pri < b.pri
end)
- -- Link "#123" to a bug tracker. Point this at your own.
- out = out:gsub("#(%d+)", '<a href="https://bugs.example.com/?bug=%1">#%1</a>')
- html(out)
+ local out = {}
+ local i = 1
+ for _, c in ipairs(cands) do
+ -- Skip a candidate that overlaps one already emitted.
+ if c.s >= i then
+ out[#out + 1] = string.sub(text, i, c.s - 1)
+ out[#out + 1] = c.link
+ i = c.e + 1
+ end
+ end
+ out[#out + 1] = string.sub(text, i)
+ html(table.concat(out))
return 0
end