-- 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 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. 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 -- -- Patterns run against the escaped message, so '&', '<' and '>' reach them as -- '&', '<' and '>'. Match those entity spellings rather than the -- bare character, and keep a pattern from ending part way through one, since -- the matched run is what gets wrapped in the anchor. 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 '' .. display .. '' 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(...) chunks = {} end function filter_write(str) chunks[#chunks + 1] = str 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 end return a.pri < b.pri 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 end end out[#out + 1] = string.sub(text, i) html(table.concat(out)) return 0 end