-- cgit trailer-filter that turns the value of a commit trailer into a link, -- named by the trailer-filter or repo.trailer-filter setting in cgitrc. cgit -- opens it once per trailer with the trailer key and the page name as its -- arguments and hands over the value already HTML-escaped, so all this does -- is wrap the value, or the part of it a rule captures, in an anchor. Runs on -- Lua 5.1 through 5.4 and LuaJIT with nothing outside the standard library. -- -- trailer-filter=lua:/path/to/link-trailers.lua -- -- Trailers whose value is a person, such as Signed-off-by, never reach this -- filter. cgit writes those through the email filter instead. -- Rules are tried in order and the first whose key matches the trailer, case -- insensitively, and whose pattern matches the value wins. Each pattern is a -- Lua pattern with a single capture and a URL where %s is replaced by that -- capture, percent-encoded. The matched run is what gets wrapped and the -- capture is only what goes into the URL. A rule without a key applies to -- every trailer. -- -- Lua patterns are not regular expressions. The reference is -- https://www.lua.org/manual/5.1/manual.html#5.4.1 -- -- Patterns run against the escaped value, so match the entity spellings -- '&', '<' and '>' rather than the bare characters, and keep a -- pattern from ending part way through one. local rules = { -- Fixes: 1234567 ("subject") and Reverts: 1234567 link the object name -- to its commit page. The relative form is resolved against the -- current page and works for the common virtual-root layout. { key = "Fixes", pattern = "^(%x%x%x%x%x%x%x+)", url = "./?id=%s" }, { key = "Reverts", pattern = "^(%x%x%x%x%x%x%x+)", url = "./?id=%s" }, -- Closes: #123 and Bug: 123 point at the tracker. { key = "Closes", pattern = "^#?(%d+)$", url = "https://bugs.example.com/?bug=%s" }, { key = "Bug", pattern = "^#?(%d+)$", url = "https://bugs.example.com/?bug=%s" }, -- { key = "CVE", pattern = "^CVE%-(%d%d%d%d%-%d+)$", -- url = "https://www.cve.org/CVERecord?id=CVE-%s" }, } -- A value that is nothing but one http or https URL, under any key, links to -- itself. Set false to leave such values as text. local link_urls = true local key = "" local chunks = {} -- 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 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 "" .. display .. "" end -- The value is already escaped, which is also what an attribute wants, so a -- URL only has to be kept away from the quote that closes the attribute. local function link_url(text) if not link_urls or not string.find(text, "^https?://[^%s'\"]+$") then return nil end return "" .. text .. "" end local function link_rule(text) local wanted = string.lower(key) for _, rule in ipairs(rules) do if rule.key == nil or string.lower(rule.key) == wanted then -- A malformed pattern is an operator error, so skip -- that rule rather than fail the whole page. local ok, start, stop, capture = pcall(string.find, text, rule.pattern) if ok and start then if capture == nil then capture = string.sub(text, start, stop) end return string.sub(text, 1, start - 1) .. make_link(rule.url, capture, string.sub(text, start, stop)) .. string.sub(text, stop + 1) end end end return nil end function filter_open(trailer_key, page) key = trailer_key or "" chunks = {} end function filter_write(str) chunks[#chunks + 1] = str end function filter_close() local text = table.concat(chunks) html(link_url(text) or link_rule(text) or text) return 0 end