diff options
context:
space:
mode:
Diffstat (limited to 'custom/extensions/link-commits.lua')
-rw-r--r--custom/extensions/link-commits.lua146
1 file changed, 73 insertions, 73 deletions
diff --git a/custom/extensions/link-commits.lua b/custom/extensions/link-commits.lua
index fabd05b..3f2429d 100644
--- a/custom/extensions/link-commits.lua
+++ b/custom/extensions/link-commits.lua
@@ -1,51 +1,45 @@
-- 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.
+-- references in a commit message into links, named by the commit-filter or
+-- repo.commit-filter setting in cgitrc. cgit hands over the message already
+-- HTML-escaped, so all this does is wrap matches in anchors. Every match is
+-- resolved in one left-to-right pass, so nothing is ever linked twice. The
+-- two tables below are the whole configuration, and the filter runs on Lua
+-- 5.1 through 5.4 and LuaJIT with nothing outside the standard library.
--
-- 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 among the values below. All matches are resolved
--- in a single left-to-right pass, so nothing is ever linked twice.
--- 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.
+-- Object names are handled apart from the rules below because the length
+-- bound on them 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, so any hex run within the bounds is linked whatever mix of digits and
+-- letters it has and abbreviated and all-digit names are both caught. The
+-- cost is that a long hex-looking number now and then links to an object that
+-- does not exist, which cgit renders as a harmless "bad object name" page.
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).
+ -- Git abbreviations run about 7 to 12 characters, and a full name is 40
+ -- characters for sha1 or 64 for 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.
+ -- Link target, where %s is replaced with the matched hash. The relative
+ -- form is resolved against 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.
+-- Text-reference rules, each one a Lua pattern with a single capture and a
+-- URL where %s is replaced by that capture, percent-encoded. The whole match
+-- is what gets shown and the capture is only what goes into the URL. Rules are
+-- tried in order and the leftmost match on the line wins, so put the more
+-- specific patterns first, and an empty list leaves only object names linked.
--
-- 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
+-- {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
+-- dash is '%-'. The whole set is in the reference manual at
+-- https://www.lua.org/manual/5.1/manual.html#5.4.1
--
-- Patterns run against the escaped message, so '&', '<' and '>' reach them as
-- '&amp;', '&lt;' and '&gt;'. Match those entity spellings rather than the
@@ -61,62 +55,67 @@ local rules = {
local chunks = {}
--- Percent-encode everything but the URL-unreserved characters, so a captured
--- value cannot break out of the href attribute or the URL.
+-- Percent-encode everything but the URL unreserved characters, so a captured
+-- value cannot break out of the href attribute or out of the URL itself.
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.
+-- Build one anchor from a URL template holding %s and the text to show. The
+-- replacement is a function so that a '%' in the encoded value is not taken
+-- for 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>'
+ 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.
+-- Collect every candidate match in the message. Priority records which rule
+-- found it, and the lower priority 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.
+ local candidates = {}
+ for priority, rule in ipairs(rules) do
+ -- A malformed pattern is an operator error, so skip that
+ -- rule rather than fail 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)
+ local start, stop, capture =
+ string.find(text, rule.pattern, init)
+ if not start then break end
+ if capture == nil then
+ capture = string.sub(text, start, stop)
end
- cands[#cands + 1] = {
- s = s, e = e, pri = pri,
- link = make_link(rule.url, cap, string.sub(text, s, e)),
+ candidates[#candidates + 1] = {
+ start = start, stop = stop, priority = priority,
+ link = make_link(rule.url, capture,
+ string.sub(text, start, stop)),
}
- init = (e >= s) and e + 1 or s + 1
+ -- An empty match still has to advance the
+ -- scan, or it never reaches the end.
+ init = (stop >= start) and stop + 1 or start + 1
end
end)
end
if objects.enabled then
- local objpri = #rules + 1
+ local priority = #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
+ local start, stop, run =
+ string.find(text, "%f[%w](%x+)%f[%W]", init)
+ if not start then break end
if #run >= objects.min_length and #run <= objects.max_length then
- cands[#cands + 1] = {
- s = s, e = e, pri = objpri,
+ candidates[#candidates + 1] = {
+ start = start, stop = stop, priority = priority,
link = make_link(objects.url, run, run),
}
end
- init = e + 1
+ init = stop + 1
end
end
- return cands
+ return candidates
end
function filter_open(...)
@@ -129,24 +128,25 @@ end
function filter_close()
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
+ local candidates = collect(text)
+ table.sort(candidates, function(a, b)
+ if a.start ~= b.start then
+ return a.start < b.start
end
- return a.pri < b.pri
+ return a.priority < b.priority
end)
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
+ local pos = 1
+ for _, candidate in ipairs(candidates) do
+ -- A candidate reaching back into one already emitted is
+ -- dropped, so no run of text is ever wrapped twice.
+ if candidate.start >= pos then
+ out[#out + 1] = string.sub(text, pos, candidate.start - 1)
+ out[#out + 1] = candidate.link
+ pos = candidate.stop + 1
end
end
- out[#out + 1] = string.sub(text, i)
+ out[#out + 1] = string.sub(text, pos)
html(table.concat(out))
return 0
end