diff options
Diffstat (limited to '')
| -rw-r--r-- | custom/extensions/link-trailers.lua | 109 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| -rw-r--r-- | tests/extensions/test-link-trailers.lua | 66 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| -rwxr-xr-x | tests/t0506-link-trailers.sh | 70 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
3 files changed, 245 insertions, 0 deletions
diff --git a/custom/extensions/link-trailers.lua b/custom/extensions/link-trailers.lua new file mode 100644 index 0000000..645c517 --- /dev/null +++ b/custom/extensions/link-trailers.lua @@ -0,0 +1,109 @@ +-- 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 "<a href='" .. href .. "'>" .. display .. "</a>" +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 "<a href='" .. text .. "'>" .. text .. "</a>" +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 diff --git a/tests/extensions/test-link-trailers.lua b/tests/extensions/test-link-trailers.lua new file mode 100644 index 0000000..c879340 --- /dev/null +++ b/tests/extensions/test-link-trailers.lua @@ -0,0 +1,66 @@ +-- Unit checks for custom/extensions/link-trailers.lua, run under a standalone +-- Lua by t0506-link-trailers.sh with the script path as the argument. The +-- shipped configuration links Fixes and Reverts object names to ./?id=, +-- Closes and Bug marks to bugs.example.com and a bare URL to itself, and +-- everything here checks that shape, the key matching and the values that +-- must pass through untouched. + +local test_directory = arg[0]:match("^(.*)/") or "." +local h = dofile(test_directory .. "/harness.lua") + +h.load(arg[1]) + +local function render(key, text) + return h.run({ key, "commit" }, { text }) +end + +local out, ret = render("Fixes", "1234567 (\"plain commit\")") +h.contains("a Fixes object name links to its commit", out, + "<a href='./?id=1234567'>1234567</a> (\"plain commit\")") +h.equals("close answers zero", ret, 0) + +out = render("fixes", "1234567") +h.contains("the key matches case insensitively", out, "<a href='./?id=1234567'>1234567</a>") + +out = render("Fixes", "123456") +h.excludes("six hex characters stay text", out, "<a") + +out = render("Reverts", "cafebabe") +h.contains("a Reverts object name links too", out, "<a href='./?id=cafebabe'>cafebabe</a>") + +out = render("Closes", "#45") +h.equals("a Closes mark links to the tracker", out, + "<a href='https://bugs.example.com/?bug=45'>#45</a>") + +out = render("Bug", "45") +h.equals("a bare Bug number links to the tracker", out, + "<a href='https://bugs.example.com/?bug=45'>45</a>") + +out = render("Closes", "not a number") +h.equals("a Closes value that is no number stays text", out, "not a number") + +out = render("Link", "https://example.com/?a=1&b=2") +h.equals("a bare URL links to itself with its escaping kept", out, + "<a href='https://example.com/?a=1&b=2'>https://example.com/?a=1&b=2</a>") + +out = render("Whatever", "https://example.com/x") +h.contains("a URL links under any key", out, "<a href='https://example.com/x'>") + +out = render("Link", "https://example.com/it's") +h.equals("a URL holding a quote stays text", out, "https://example.com/it's") + +out = render("Link", "see https://example.com/x") +h.equals("a URL inside other text stays text", out, "see https://example.com/x") + +out = render("Reported-by", "someone") +h.equals("a key with no rule passes through", out, "someone") + +out = h.run({ "Fixes", "commit" }, { "12345", "67 tail" }) +h.contains("a value split across writes is seen whole", out, + "<a href='./?id=1234567'>1234567</a> tail") + +out, ret = render("Fixes", "") +h.equals("an empty value stays empty", out, "") +h.equals("an empty value still answers zero", ret, 0) + +h.finish() diff --git a/tests/t0506-link-trailers.sh b/tests/t0506-link-trailers.sh new file mode 100755 index 0000000..d397309 --- /dev/null +++ b/tests/t0506-link-trailers.sh @@ -0,0 +1,70 @@ +#!/bin/sh + +# Checks link-trailers.lua, the shipped trailer-filter, whose configuration as +# shipped links Fixes and Reverts object names to ./?id=, Closes and Bug marks +# to bugs.example.com and a bare URL to itself. The unit checks cover the rules +# under every suitable standalone Lua, and a commit carrying one of each is +# then rendered through cgit itself. + +test_description='Check the shipped link-trailers extension' +CGIT_TEST_NO_CREATE_REPOS=YesPlease +. ./setup.sh +. "$TEST_OUTPUT_DIRECTORY/extensions/lib.sh" + +interpreters=$(ext_lua_interpreters 4) +test -z "$interpreters" && + say 'no standalone lua on the path, unit checks skipped' + +for lua in $interpreters +do + test_expect_success "unit checks under $lua" " + '$lua' '$EXT_TEST_DIRECTORY/test-link-trailers.lua' '$EXTENSIONS_DIRECTORY/link-trailers.lua' + " +done + +test "$CGIT_HAS_LUA" -eq 1 && test_set_prereq CGIT_LUA +test "$CGIT_HAS_LUA" -eq 1 || + say 'cgit built without lua, filter checks through cgit skipped' + +test_expect_success 'create a repository whose commit carries linkable trailers' ' + test_create_repo repos/linky && + ( + cd repos/linky && + echo content >file && + git add file && + git commit -F - <<-\EOF + fix it + + Fixes: 1234567 ("earlier") + Link: https://example.com/?a=1&b=2 + Closes: #45 + EOF + ) +' + +test_expect_success 'point cgit at it through the trailer filter' ' + cat >cgitrc <<-EOF + virtual-root=/ + cache-size=0 + enable-trailers=1 + enable-filter-overrides=1 + repo.url=linky + repo.path=$PWD/repos/linky/.git + repo.trailer-filter=lua:$EXTENSIONS_DIRECTORY/link-trailers.lua + EOF +' + +test_expect_success CGIT_LUA 'the Fixes object name links to its commit page' ' + cgit_url "linky/commit/" >tmp && + grep "<th>Fixes</th><td><a href=....id=1234567.>1234567</a> (.earlier.)</td>" tmp +' + +test_expect_success CGIT_LUA 'the bare URL links to itself' ' + grep "<a href=.https://example.com/?a=1&b=2.>https://example.com/?a=1&b=2</a>" tmp +' + +test_expect_success CGIT_LUA 'the Closes mark links out' ' + grep "bugs.example.com/?bug=45.>#45</a>" tmp +' + +test_done |
