Diff options
Diffstat (limited to '')
-rw-r--r--custom/cgitrc103
-rw-r--r--custom/extensions/about-render.lua259
-rw-r--r--custom/extensions/auth-file.lua127
-rw-r--r--custom/extensions/auth-inline.lua123
-rw-r--r--custom/extensions/email-gravatar.lua11
-rw-r--r--custom/extensions/email-libravatar.lua11
-rw-r--r--custom/extensions/link-commits.lua10
-rw-r--r--custom/extensions/link-trailers.lua14
-rw-r--r--custom/extensions/syntax-highlight.lua67
-rwxr-xr-xcustom/hooks/post-receive.cgit-age15
-rwxr-xr-xcustom/hooks/post-receive.cgit-cache4
-rwxr-xr-xcustom/hooks/post-receive.commit-graph2
-rw-r--r--custom/servers/apache.conf49
-rw-r--r--custom/servers/lighttpd.conf22
-rw-r--r--custom/servers/nginx.conf52
15 files changed, 290 insertions, 579 deletions
diff --git a/custom/cgitrc b/custom/cgitrc
index 86f00aa..c1a5b68 100644
--- a/custom/cgitrc
+++ b/custom/cgitrc
@@ -10,10 +10,6 @@
# list repositories by hand in the per-repository section at the end of this
# file.
#
-# About pages (markdown, man and plain-text readmes) can render through the
-# bundled about-render.lua about-filter in the filter section below. Source
-# syntax highlighting is optional and ships as a source-filter there too.
-#
# One key=value pair per line. Lines starting with # or ; are comments.
# Maximum number of entries in the cgit output cache. A value of 0 disables
@@ -23,36 +19,33 @@ cache-size=0
# Directory used to store cgit cache entries. Value is a filesystem path that
# may use macros. Default is /var/cache/cgit. The directory must be owned by the
# account the web server runs cgit as, with mode 0700, since the cache holds
-# every rendered page and the URLs visitors asked for.
+# every rendered page and the urls visitors asked for.
cache-root=/var/cache/cgit
-# Minutes to cache the repository index page. Value is an integer number of
-# minutes. Default is 5.
+# Minutes to cache the repository index page. Value is an integer. Default is 5.
cache-index-ttl=5
-# Minutes to cache the repository summary page. Value is an integer number of
-# minutes. Default is 5.
+# Minutes to cache the repository summary page. Value is an integer. Default is
+# 5.
cache-summary-ttl=5
# Minutes to cache the result of scanning a path for repositories. Value is an
-# integer number of minutes. Default is 15.
+# integer. Default is 15.
cache-scan-ttl=15
# Minutes to cache repository pages requested with a fixed object id. A negative
-# value keeps the page forever. Value is an integer number of minutes. Default
-# is -1.
+# value keeps the page forever. Value is an integer. Default is -1.
cache-static-ttl=-1
# Minutes to cache repository pages requested without a fixed object id. Value
-# is an integer number of minutes. Default is 5.
+# is an integer. Default is 5.
cache-dynamic-ttl=5
-# Minutes to cache the repository about page. Value is an integer number of
-# minutes. Default is 15.
+# Minutes to cache the repository about page. Value is an integer. Default is
+# 15.
cache-about-ttl=15
-# Minutes to cache snapshots. Value is an integer number of minutes. Default is
-# 5.
+# Minutes to cache snapshots. Value is an integer. Default is 5.
cache-snapshot-ttl=5
# Largest response, in kilobytes, that a cache slot keeps. A larger one is
@@ -82,12 +75,12 @@ root-desc=a fast webinterface for the git dscm
#readme=:README.md
# CSS document urls added to the head of every page. An empty value disables it.
-# Value is one or more urls. Default is /cgit.css.
+# Value is a url, and each css line adds one. Default is /cgit.css.
css=/cgit.css
# JavaScript document urls included on every page, loaded with defer in the
-# order given. An empty value disables it. Value is one or more urls. Default
-# is /cgit.js.
+# order given. An empty value disables it. Value is a url, and each js line
+# adds one. Default is /cgit.js.
js=/cgit.js
# Shortcut icon url for all pages. Value is a url. Default is /favicon.ico.
@@ -124,8 +117,8 @@ enable-header=1
# or 1. Default is 0.
embedded=0
-# Root url for all cgit links, enabling virtual urls. Value is a url path.
-# Default is none.
+# Root url for all cgit links. Value is a url path. Default is SCRIPT_NAME
+# followed by a slash.
#virtual-root=/
# Link format string for submodules in directory listings. Value is a format
@@ -133,24 +126,21 @@ embedded=0
# Default is none.
#module-link=/%s/commit/?id=%s
-# Directory scanned for git repositories at parse time. Value is a filesystem
-# path that may use macros. Default is none.
-#scan-path=/var/lib/git
-
# When scanning, recurse into dot-directories. Must precede scan-path. Values
# are 0 or 1. Default is 0.
scan-hidden-path=0
-# Derive default section names from N leading path elements. Must precede
-# scan-path. Value is an integer that may be negative. Default is 0.
+# Derive default section names from the first N path elements, or from all
+# but the last -N when N is negative. Must precede scan-path. Value is an
+# integer. Default is 0.
section-from-path=0
# Strip the .git suffix from a scanned repository's url and name. Must precede
# scan-path. Values are 0 or 1. Default is 0.
remove-suffix=0
-# File listing subdirs of scan-path to load. Must precede scan-path. Value is a
-# filesystem path that may use macros. Default is none.
+# File listing subdirectories of scan-path to load. Must precede scan-path.
+# Value is a filesystem path that may use macros. Default is none.
#project-list=/var/lib/git/projects.list
# Serve a repository only if this file exists inside it. Emulates gitweb
@@ -233,18 +223,17 @@ enable-remote-branches=0
# 1. Default is 0.
enable-subject-links=0
-# Allow /plain to serve HTML mimetypes instead of downgrading to text/plain.
-# Values are 0 or 1. Default is 0.
+# Allow /plain to serve HTML mimetypes instead of downgrading to text/plain or
+# application/octet-stream. Values are 0 or 1. Default is 0.
enable-html-serving=0
# Generate line-number links for plaintext blobs in the tree and blame pages.
# Values are 0 or 1. Default is 1.
enable-tree-linenumbers=1
-# Name the cgit and git versions in the page footer, the generator tag and
-# the signature of patches. Exact versions help someone match a site against
-# advisories, so they stay out unless asked for. Values are 0 or 1. Default
-# is 0.
+# Name the cgit version in the page footer, the generator tag and patch
+# signatures, and the git version in the footer. Values are 0 or 1.
+# Default is 0.
enable-version-info=0
# List directories first, then files, in the tree page. Values are 0 or 1.
@@ -281,44 +270,38 @@ enable-http-clone=1
# repository ships under custom/extensions/. The ones written as
# /path/to/your-command mark where your own command goes.
-# Honour every setting in a repository's own cgitrc and git config found by
-# scan-path. Those files belong to whoever can push, so without this the
-# settings that run a command, put raw markup on the page, serve a file as
-# markup, place a link or read a file off the disk are ignored. The repo.* lines
-# below never need it. Values are 0 or 1. Default is 0.
+# Honour every setting in a scanned repository's own cgitrc and git config,
+# including those that run a command or put markup on the page. Those files
+# belong to whoever can push. The repo.* lines below never need it. Values are
+# 0 or 1. Default is 0.
trust-scan-config=0
# Filter command used to format about-page content. The bundled about-render.lua
-# renders markdown, man pages and plain text. Value is a command optionally
-# prefixed with exec or lua. Default is none.
+# renders markdown, man pages and plain text. Value is a command. Default is
+# none.
#about-filter=lua:/usr/local/lib/cgit/filters/about-render.lua
# Filter command used to format commit messages, for example to turn object
-# names and issue numbers into links. Value is a command optionally prefixed
-# with exec or lua. Default is none.
+# names and issue numbers into links. Value is a command. Default is none.
#commit-filter=lua:/usr/local/lib/cgit/filters/link-commits.lua
# Filter command used to format author and committer emails, for example to add
-# avatar images. Value is a command optionally prefixed with exec or lua.
-# Default is none.
+# avatar images. Value is a command. Default is none.
#email-filter=lua:/usr/local/lib/cgit/filters/email-gravatar.lua
# Filter command used to format the value of a commit trailer when
# enable-commit-trailers is 1. The shipped filter links Fixes, Reverts, Closes,
-# Bug and URL values. Value is a command optionally prefixed with exec or lua.
-# Default is none.
+# Bug and URL values. Value is a command. Default is none.
#trailer-filter=lua:/usr/local/lib/cgit/filters/link-trailers.lua
# Filter command used to format plaintext blobs in the tree and blame pages. The
-# shipped filter uses the Scintillua lexers and needs lpeg, and missing either
-# means plain uncolored text, not an error. Value is a command optionally
-# prefixed with exec or lua. Default is none.
+# comments in custom/extensions/syntax-highlight.lua list what the shipped
+# filter needs. Value is a command. Default is none.
#source-filter=lua:/usr/local/lib/cgit/filters/syntax-highlight.lua
# Filter command invoked to authenticate access, gating repositories behind a
# login. See custom/extensions/auth-inline.lua and
-# custom/extensions/auth-file.lua. Value is a command optionally prefixed with
-# exec or lua. Default is none.
+# custom/extensions/auth-file.lua. Value is a command. Default is none.
#auth-filter=lua:/usr/local/lib/cgit/filters/auth-inline.lua
# Number of items to display in atom feeds. Value is an integer. Default is 10.
@@ -332,7 +315,7 @@ max-message-length=80
# integer. Default is 80.
max-repodesc-length=80
-# Max blob size in KBytes read into memory for html, plain and blob output. A
+# Max blob size in KBytes read into memory for HTML, plain and blob output. A
# value of 0 disables the limit. Value is an integer. Default is 10240.
max-blob-size=10240
@@ -361,8 +344,7 @@ max-diff-lines=1000
# Branches and tags listed per section on the refs page, and per page on the
# dedicated branch and tag pages. A value of 0 or less lists everything on one
-# page.
-# Value is an integer. Default is 200.
+# page. Value is an integer. Default is 200.
max-ref-count=200
# Max files considered when detecting renames. A value of -1 uses the git
@@ -426,6 +408,11 @@ age-file=info/web/last-modified
#mimetype.png=image/png
#mimetype.svg=image/svg+xml
+# Directory scanned for git repositories at parse time. Each repository found
+# inherits the global settings above this line, so it comes after them all.
+# Value is a filesystem path that may use macros. Default is none.
+#scan-path=/var/lib/git
+
# Each repository is introduced by a repo.url line. All following repo.*
# settings apply to that repository until the next repo.url line. When
# repositories are discovered via scan-path the repo. prefix is dropped inside
@@ -462,7 +449,7 @@ age-file=info/web/last-modified
#repo.section=main
# Prefix for snapshot filenames instead of the repository basename. Value is a
-# prefix string. Default is none.
+# prefix string. Default is the repository basename.
#repo.snapshot-prefix=example
# Extra content added verbatim to the head of each page for this repository.
diff --git a/custom/extensions/about-render.lua b/custom/extensions/about-render.lua
index 808dd08..5738b00 100644
--- a/custom/extensions/about-render.lua
+++ b/custom/extensions/about-render.lua
@@ -3,8 +3,9 @@
-- interpreter so a readme costs no extra process per request. Markdown, man
-- pages and plain text are the three formats, chosen from the readme's file
-- extension, and anything that fails to parse falls back to escaped plain
--- text. The wrappers it emits carry the classes that assets/cgit.css styles.
--- It runs on Lua 5.1 through 5.5 and LuaJIT.
+-- text. Markdown and man pages come out in div.markdown and plain text in
+-- pre.plaintext, the two wrappers assets/cgit.css styles. It runs on Lua 5.1
+-- through 5.5 and LuaJIT.
--
-- about-filter=lua:/usr/local/lib/cgit/filters/about-render.lua
@@ -190,9 +191,7 @@ end
-- first. html() only ever gets literal tag scaffolding and the character
-- references whose shape the markdown grammar checked.
-local emit_inline
-
-emit_inline = function(list)
+local function emit_inline(list)
for _, node in ipairs(list) do
local kind = node.kind
if kind == "text" then
@@ -271,9 +270,7 @@ local function emit_lines(lines)
end
end
-local emit_blocks
-
-emit_blocks = function(blocks)
+local function emit_blocks(blocks)
for _, block in ipairs(blocks) do
local kind = block.kind
if kind == "heading" then
@@ -285,13 +282,7 @@ emit_blocks = function(blocks)
elseif kind == "hr" then
html("<hr>")
elseif kind == "code" then
- html("<pre><code")
- if block.lang and block.lang ~= "" then
- html(" data-lang='")
- html_attr(block.lang)
- html("'")
- end
- html(">")
+ html("<pre><code>")
html_txt(block.text)
html("</code></pre>")
elseif kind == "blockquote" then
@@ -348,10 +339,23 @@ end
-- Both parse into a node tree before anything is emitted, so a failed parse
-- falls back to plain text without leaving half a page behind.
+local function render_blocks(text, parse)
+ local ok, blocks = pcall(parse, text)
+ if not ok then
+ return render_plaintext(text)
+ end
+ html("<div class='markdown'>")
+ -- Emitting is wrapped so a bug there can at worst truncate the page,
+ -- never end the request in an error.
+ pcall(emit_blocks, blocks)
+ html("</div>")
+end
+
local render_markdown, render_man
if has_lpeg then
- local P, R, S, B, C, Ct = lpeg.P, lpeg.R, lpeg.S, lpeg.B, lpeg.C, lpeg.Ct
+ local P, R, S, B = lpeg.P, lpeg.R, lpeg.S, lpeg.B
+ local C, Cc, Ct = lpeg.C, lpeg.Cc, lpeg.Ct
local space = S(" \t")
local whitespace = S(" \t\r\n\f\v")
@@ -447,28 +451,27 @@ if has_lpeg then
return { node_text(s) }
end
inline_depth = inline_depth + 1
- local nodes = inline_grammar:match(s) or { node_text(s) }
+ local nodes = inline_grammar:match(s)
inline_depth = inline_depth - 1
return nodes
end
- local lang_char = R("az", "AZ", "09") + S("_.+#-")
local heading_line = C(P("#") * P("#") ^ -5) * space ^ 1 * C(P(1) ^ 0)
-- A run of equals signs or dashes under a paragraph turns it into a
-- heading, as on GitHub, rather than reading as more of its text.
- local setext_line = space ^ 0 * (C(P("=") ^ 1) + C(P("-") ^ 1))
+ local setext_line = space ^ 0 * (P("=") ^ 1 * Cc(1) + P("-") ^ 1 * Cc(2))
* space ^ 0 * eol
local code_indent = P(" ") + P("\t")
local function thematic(mark)
return space ^ 0 * P(mark) * (space ^ 0 * P(mark)) ^ 2 * space ^ 0 * eol
end
local break_line = thematic("-") + thematic("*") + thematic("_")
- local fence_line = space ^ 0 * (C(P("`") ^ 3) + C(P("~") ^ 3)) * space ^ 0 * C(lang_char ^ 0)
+ local fence_line = space ^ 0 * (C(P("`") ^ 3) + C(P("~") ^ 3))
local close_backtick = space ^ 0 * P("`") ^ 3 * space ^ 0 * eol
local close_tilde = space ^ 0 * P("~") ^ 3 * space ^ 0 * eol
local quote_line = space ^ 0 * P(">")
local bullet = S("-*+")
- local number = R("09") ^ 1 * S(".)")
+ local number = digit ^ 1 * S(".)")
local item_line = space ^ 0 * C(bullet + number) * space ^ 1 * C(P(1) ^ 0)
local dash_cell = space ^ 0 * P(":") ^ -1 * P("-") ^ 1 * P(":") ^ -1 * space ^ 0
-- A delimiter row is dash-cells joined by pipes. Require at least one
@@ -480,16 +483,10 @@ if has_lpeg then
pipe * dash_cell * (pipe * dash_cell) ^ 0 * pipe ^ -1
+ dash_cell * (pipe * dash_cell) ^ 1 * pipe ^ -1
) * space ^ 0 * eol
- local block_start = space ^ 0 * ((P("#") * P("#") ^ -5 * space) + P(">")
- + P("`") ^ 3 + P("~") ^ 3 + ((bullet + number) * space))
+ local block_start = space ^ 0 * heading_line + quote_line + fence_line + item_line
local function ordered(mark) return mark:match("%d") ~= nil end
- local function follows_list(blocks)
- local last = blocks[#blocks]
- return last ~= nil and last.kind == "list"
- end
-
-- An indented block runs on through blank lines, and up to four spaces
-- or one tab come off each of its lines.
local function continues_code(line)
@@ -500,13 +497,6 @@ if has_lpeg then
return line:match("^\t(.*)$") or line:match("^ ? ? ? ?(.*)$")
end
- local function setext_level(line)
- local run = setext_line:match(line)
- if run then
- return run:sub(1, 1) == "=" and 1 or 2
- end
- end
-
local function table_starts(lines, i)
return i < #lines and lines[i]:find("|", 1, true) ~= nil
and table_delimiter:match(lines[i + 1]) ~= nil
@@ -517,7 +507,15 @@ if has_lpeg then
-- paragraph short.
local function ends_paragraph(lines, i)
return lines[i]:match("^%s*$") or block_start:match(lines[i])
- or table_starts(lines, i)
+ or break_line:match(lines[i]) or table_starts(lines, i)
+ end
+
+ local function code_block(lines)
+ local text = table.concat(lines, "\n")
+ if #lines > 0 then
+ text = text .. "\n"
+ end
+ return { kind = "code", text = text }
end
-- A paragraph's lines run together the way GitHub shows them, unless one
@@ -542,13 +540,11 @@ if has_lpeg then
return lines
end
- local parse_blocks
-
- parse_blocks = function(lines, depth)
+ local function parse_blocks(lines, depth)
local blocks, i, n = {}, 1, #lines
while i <= n do
local line = lines[i]
- local fence, lang = fence_line:match(line)
+ local fence = fence_line:match(line)
local hashes, title = heading_line:match(line)
local mark = item_line:match(line)
if line:match("^%s*$") then
@@ -562,15 +558,7 @@ if has_lpeg then
i = i + 1
end
i = i + 1
- local body = table.concat(code, "\n")
- if #code > 0 then
- body = body .. "\n"
- end
- blocks[#blocks + 1] = {
- kind = "code",
- lang = lang,
- text = body,
- }
+ blocks[#blocks + 1] = code_block(code)
elseif hashes then
blocks[#blocks + 1] = {
kind = "heading",
@@ -628,10 +616,8 @@ if has_lpeg then
end
local item = { content }
i = i + 1
- -- A wrapped line carries the item on, whatever its
- -- indent, while a rule still ends the list.
- while i <= n and not ends_paragraph(lines, i)
- and not break_line:match(lines[i]) do
+ -- A wrapped line carries the item on, whatever its indent.
+ while i <= n and not ends_paragraph(lines, i) do
item[#item + 1] = lines[i]
i = i + 1
end
@@ -645,7 +631,8 @@ if has_lpeg then
-- Four spaces or a tab start a code block, except straight
-- after a list, where an indented paragraph more likely
-- carries an item on.
- elseif code_indent:match(line) and not follows_list(blocks) then
+ elseif code_indent:match(line)
+ and (blocks[#blocks] or {}).kind ~= "list" then
local code = {}
while i <= n and continues_code(lines[i]) do
code[#code + 1] = unindent(lines[i])
@@ -654,17 +641,13 @@ if has_lpeg then
while code[#code]:match("^%s*$") do
code[#code] = nil
end
- blocks[#blocks + 1] = {
- kind = "code",
- text = table.concat(code, "\n") .. "\n",
- }
+ blocks[#blocks + 1] = code_block(code)
else
local paragraph, level = { line }, nil
i = i + 1
while i <= n do
- level = setext_level(lines[i])
- if level or ends_paragraph(lines, i)
- or break_line:match(lines[i]) then
+ level = setext_line:match(lines[i])
+ if level or ends_paragraph(lines, i) then
break
end
paragraph[#paragraph + 1] = lines[i]
@@ -693,19 +676,9 @@ if has_lpeg then
end
render_markdown = function(text)
- if #text > max_bytes then
- return render_plaintext(text)
- end
- local ok, blocks = pcall(parse_blocks, split_lines(text), 0)
- if not ok then
- return render_plaintext(text)
- end
- html("<div class='markdown'>")
- -- Emitting is wrapped so a bug there can at worst
- -- truncate the page, never raise and let the outer
- -- fallback duplicate what has already been sent.
- pcall(emit_blocks, blocks)
- html("</div>")
+ render_blocks(text, function(t)
+ return parse_blocks(split_lines(t), 0)
+ end)
end
-- Roff fonts are a current state carried across a run rather than
@@ -733,15 +706,8 @@ if has_lpeg then
}
local backslash = P("\\")
- local function text_token(text) return { kind = "text", text = text } end
- local skip_token = { kind = "skip" }
- local function skip() return skip_token end
local function font_token(name)
- return { kind = "font", font = fonts[name] or "R" }
- end
- local function char_token(name) return text_token(man_chars[name] or "") end
- local function string_token(name)
- return text_token(man_strings[name] or "")
+ return { font = fonts[name] or "R" }
end
-- \( takes a two character name and \[ a longer one, bounded so that a
@@ -754,50 +720,43 @@ if has_lpeg then
-- the escapes that only steer hyphenation or breaks.
local man_inline_grammar = Ct((
(backslash * P("f") * (named + C(P(1)))) / font_token
- + (backslash * S('"#') * P(1) ^ 0) / skip
- + (backslash * P("-")) / function() return text_token("-") end
- + (backslash * P("e")) / function() return text_token("\\") end
- + (backslash * S(" 0~")) / function() return text_token(" ") end
- + (backslash * S("&|^%:cud{}")) / skip
- + (backslash * named) / char_token
- + (backslash * P("*") * (named + C(P(1)))) / string_token
- + (backslash * P("s") * S("+-") ^ -1 * (named + R("09") * R("09") ^ -1)) / skip
- + (backslash * S("mM") * (named + P(1))) / skip
- + (backslash * C(P(1))) / text_token
- + backslash / skip
- + C((1 - backslash) ^ 1) / text_token
+ + backslash * S('"#') * P(1) ^ 0
+ + (backslash * P("-")) / "-"
+ + (backslash * P("e")) / "\\"
+ + (backslash * S(" 0~")) / " "
+ + backslash * S("&|^%:cud{}")
+ + (backslash * named) / man_chars
+ + (backslash * P("*") * (named + C(P(1)))) / man_strings
+ + (backslash * P("s") * S("+-") ^ -1 * (named + digit * digit ^ -1)) / 0
+ + (backslash * S("mM") * (named + P(1))) / 0
+ + backslash * C(P(1))
+ + backslash
+ + C((1 - backslash) ^ 1)
) ^ 0)
+ local function in_font(font, nodes)
+ if font == "B" then
+ return { { kind = "strong", kids = nodes } }
+ elseif font == "I" then
+ return { { kind = "em", kids = nodes } }
+ end
+ return nodes
+ end
+
local function man_inline(text)
- local tokens = man_inline_grammar:match(text) or {}
+ local tokens = man_inline_grammar:match(text)
local nodes, parts, font = {}, {}, "R"
local function flush()
- if #parts == 0 then
- return
- end
local run = table.concat(parts)
parts = {}
- if run == "" then
- return
- end
- if font == "B" then
- nodes[#nodes + 1] = {
- kind = "strong",
- kids = { { kind = "text", text = run } },
- }
- elseif font == "I" then
- nodes[#nodes + 1] = {
- kind = "em",
- kids = { { kind = "text", text = run } },
- }
- else
- nodes[#nodes + 1] = { kind = "text", text = run }
+ if run ~= "" then
+ nodes[#nodes + 1] = in_font(font, { node_text(run) })[1]
end
end
for _, token in ipairs(tokens) do
- if token.kind == "text" then
- parts[#parts + 1] = token.text
- elseif token.kind == "font" then
+ if type(token) == "string" then
+ parts[#parts + 1] = token
+ else
flush()
font = token.font
end
@@ -809,13 +768,7 @@ if has_lpeg then
-- A no fill block keeps its lines as they are, so only the text of
-- each one is wanted there, without its fonts.
local function man_plain(text)
- local parts = {}
- for _, token in ipairs(man_inline_grammar:match(text) or {}) do
- if token.kind == "text" then
- parts[#parts + 1] = token.text
- end
- end
- return table.concat(parts)
+ return table.concat(plain_text(man_inline(text), {}))
end
-- A macro's arguments divide at blanks, except inside double quotes.
@@ -838,15 +791,6 @@ if has_lpeg then
end
end
- local function in_font(font, nodes)
- if font == "B" then
- return { { kind = "strong", kids = nodes } }
- elseif font == "I" then
- return { { kind = "em", kids = nodes } }
- end
- return nodes
- end
-
-- Each alternating macro sets its arguments in its two fonts by turns
-- with nothing between them, as in .BR ls (1).
local single_font = { B = "B", I = "I" }
@@ -883,11 +827,7 @@ if has_lpeg then
end
local function flush_nofill()
if nofill_lines then
- local body = table.concat(nofill_lines, "\n")
- if #nofill_lines > 0 then
- body = body .. "\n"
- end
- blocks[#blocks + 1] = { kind = "code", text = body }
+ blocks[#blocks + 1] = code_block(nofill_lines)
nofill_lines = nil
end
end
@@ -1000,22 +940,8 @@ if has_lpeg then
end
render_man = function(text)
- if #text > max_bytes then
- return render_plaintext(text)
- end
- local ok, blocks = pcall(parse_man, text)
- if not ok then
- return render_plaintext(text)
- end
- html("<div class='markdown manpage'>")
- pcall(emit_blocks, blocks)
- html("</div>")
+ render_blocks(text, parse_man)
end
-else
- -- No lpeg, so markdown and man readmes read as escaped source, not
- -- nothing.
- render_markdown = render_plaintext
- render_man = render_plaintext
end
@@ -1043,32 +969,13 @@ end
-- cgit takes filter output through a C string sink that stops at the first
-- NUL byte, so each one becomes U+FFFD, as a browser would show it, and the
--- text past it survives. A plain find works on Lua 5.1, where a NUL cannot
--- sit in a pattern.
-local function replace_nul(text)
- if not text:find("\0", 1, true) then
- return text
- end
- local parts, start = {}, 1
- while true do
- local at = text:find("\0", start, true)
- if not at then
- parts[#parts + 1] = text:sub(start)
- return table.concat(parts, "\239\191\189")
- end
- parts[#parts + 1] = text:sub(start, at - 1)
- start = at + 1
- end
-end
-
+-- text past it survives. The class names NUL as the one byte outside \1 to
+-- \255, since on Lua 5.1 a zero byte in a pattern ends it.
function filter_close()
- local text = replace_nul(table.concat(chunks))
+ local text = table.concat(chunks):gsub("[^\1-\255]", "\239\191\189")
chunks = {}
local ext = (filename:match("%.([^.]+)$") or ""):lower()
- local render = handlers[ext] or render_plaintext
- local ok = pcall(render, text)
- if not ok then
- render_plaintext(text)
- end
+ local render = #text <= max_bytes and handlers[ext] or render_plaintext
+ render(text)
return 0
end
diff --git a/custom/extensions/auth-file.lua b/custom/extensions/auth-file.lua
index a03e1ea..ab56ee1 100644
--- a/custom/extensions/auth-file.lua
+++ b/custom/extensions/auth-file.lua
@@ -53,7 +53,6 @@
-- still lists a protected repository with its description unless cgitrc sets
-- repo.hide=1 for it.
--- The values that follow are the configuration and are meant to be edited.
-- Accounts live one per line as username:hash, where the hash is any kind this
-- system's crypt(3) checks. yescrypt ($y$) is what libxcrypt recommends, as on
@@ -79,8 +78,8 @@ local groups_filename = "/etc/cgit-auth/groups"
-- Per-repository access lives one per line as reponame:group1,group2 and so
-- on. A repository named here is protected and one that is not named is
-- public. The repository name has to match exactly, while group and user names
--- match regardless of case. While this file cannot be read every repository
--- is refused, since nothing then tells the protected ones apart.
+-- match regardless of case. While this file cannot be read every page, the
+-- index too, is refused, since nothing then tells the protected ones apart.
local repos_filename = "/etc/cgit-auth/repos"
-- Where the cookie-signing secret is stored, created on first use. It must be
@@ -105,6 +104,7 @@ local cookie_insecure = false
-- the password's length, and one longer than this is turned down unchecked.
local max_password_bytes = 256
+
-- A browser accepts a cookie named __Host- only when it is Secure, covers the
-- whole host and was set by this host, so no sibling subdomain can plant a
-- session in its place. Either cookie setting above can rule that out.
@@ -113,12 +113,18 @@ if not cookie_insecure and cookie_path == "/" then
session_cookie = "__Host-" .. cookie_name
end
+-- A cookie carries its expiry and Max-Age as whole seconds, while on Lua 5.3
+-- and later a length worked out with a fraction, such as 1.5 days, is a float
+-- that tostring would write with a trailing ".0".
+session_seconds = math.floor(session_seconds)
+
-- One request reaches this script as an open, a write and a close, so what the
-- open decodes is kept here for the calls that follow.
local action, http, cgit, post
--- The two lookups below, account_hash and repo_userset, are the only part
--- of this script that differs from auth-inline.lua.
+-- Apart from the configuration, the lookups below, account_hash and
+-- repo_userset with their helpers, are the only code that differs from
+-- auth-inline.lua, which defines trim later, beside get_cookie.
-- Lua patterns backtrack, so "^%s*(.-)%s*$" costs the square of a run of
-- spaces inside the text. Finding each end directly stays linear, which
@@ -152,9 +158,6 @@ end
-- a failed login can spend the same work on a hash of the same kind and
-- cost, and its timing does not tell whether the name exists.
function account_hash(user)
- if user == nil then
- return nil
- end
local wanted = user:lower()
local users_file = io.open(users_filename, "r")
if users_file == nil then
@@ -180,9 +183,6 @@ end
-- repository whose groups resolve to nobody comes back as an empty table
-- instead, so it denies everyone rather than falling through to public.
function repo_userset(repo)
- if repo == nil then
- return nil
- end
local groups = nil
local repos_file, err = io.open(repos_filename, "r")
if repos_file == nil then
@@ -215,25 +215,17 @@ function repo_userset(repo)
return users
end
--- The URL helpers below are adapted from keplerproject/wsapi.
+-- The url helpers below are adapted from keplerproject/wsapi.
function url_decode(str)
- if not str then
- return ""
- end
str = string.gsub(str, "+", " ")
str = string.gsub(str, "%%(%x%x)", function(hex)
return string.char(tonumber(hex, 16))
end)
- str = string.gsub(str, "\r\n", "\n")
return str
end
function url_encode(str)
- if not str then
- return ""
- end
- str = string.gsub(str, "\n", "\r\n")
str = string.gsub(str, "([^%w ])", function(char)
return string.format("%%%02X", string.byte(char))
end)
@@ -245,13 +237,13 @@ end
-- '=', for example a base64 password, so the value runs to the next '&'.
function parse_query(query)
local params = {}
- for key, value in string.gmatch(query or "", "([^&=]+)=([^&]*)") do
+ for key, value in string.gmatch(query, "([^&=]+)=([^&]*)") do
params[url_decode(key)] = url_decode(value)
end
return params
end
--- The token comes back still URL encoded by secure_value. Decoding it here
+-- The token comes back still url encoded by secure_value. Decoding it here
-- would break the signature check for any value carrying a percent escape.
function get_cookie(cookies, name)
for raw in string.gmatch(cookies or "", "[^;]+") do
@@ -350,46 +342,9 @@ end
-- or to nil to turn it down, which is how a session meets its account's
-- current hash.
function validate_value(expected_field, cookie, bind)
- local i = 0
- local value = ""
- local field = ""
- local expiration = 0
- local salt = ""
- local signature = ""
-
- if cookie == nil or cookie:len() < 3 or cookie:sub(1, 1) == "|" then
- return nil
- end
-
- for component in string.gmatch(cookie, "[^|]+") do
- if i == 0 then
- field = component
- elseif i == 1 then
- value = component
- elseif i == 2 then
- -- The expiration must be a plain integer, since
- -- tonumber and tostring of "1e9" or "100.0" differ
- -- across Lua versions and the signed bytes have to
- -- come back byte for byte.
- if not string.match(component, "^%d+$") then
- return nil
- end
- expiration = tonumber(component)
- elseif i == 3 then
- salt = component
- elseif i == 4 then
- signature = component
- else
- break
- end
- i = i + 1
- end
-
- if signature == nil or signature:len() == 0 then
- return nil
- end
-
- if url_decode(field) ~= expected_field then
+ local field, value, expiration, salt, signature =
+ string.match(cookie or "", "^([^|]+)|([^|]+)|(%d+)|([^|]+)|([^|]+)$")
+ if signature == nil or url_decode(field) ~= expected_field then
return nil
end
@@ -408,12 +363,13 @@ function validate_value(expected_field, cookie, bind)
end
end
- local payload = field .. "|" .. value .. "|" .. tostring(expiration) .. "|" .. salt
+ local payload = field .. "|" .. value .. "|" .. expiration .. "|" .. salt
if not constant_equals(signature, sign(payload, binding)) then
return nil
end
-- An expiration of 0 never expires and is used for the redirect token.
+ expiration = tonumber(expiration)
if expiration ~= 0 and expiration <= os.time() then
return nil
end
@@ -435,12 +391,6 @@ function secure_value(field, value, expiration, binding)
return payload .. "|" .. sign(payload, binding)
end
--- A control character in a header value would let that value split the
--- response, so it is dropped rather than escaped.
-function strip_controls(s)
- return (string.gsub(s or "", "%c", ""))
-end
-
-- Compare two strings in time that does not depend on how many leading bytes
-- match, so a mismatch position is not revealed by timing.
function constant_equals(a, b)
@@ -456,7 +406,8 @@ function constant_equals(a, b)
end
-- An empty value is how a login is cleared, since Max-Age=0 tells the browser
--- to drop the cookie it already holds.
+-- to drop the cookie it already holds. Any other value is a token secure_value
+-- built, which holds no control character to split the response with.
function set_cookie(cookie, value)
local attributes = "; HttpOnly; SameSite=Lax; Path=" .. cookie_path
if not cookie_insecure then
@@ -464,16 +415,18 @@ function set_cookie(cookie, value)
end
if value == "" then
attributes = attributes .. "; Max-Age=0"
- elseif session_seconds > 0 then
+ else
attributes = attributes .. "; Max-Age=" .. tostring(session_seconds)
end
- html("Set-Cookie: " .. cookie .. "=" .. strip_controls(value) .. attributes .. "\n")
+ html("Set-Cookie: " .. cookie .. "=" .. value .. attributes .. "\n")
end
+-- A control character in the location would let it split the response, so
+-- it is dropped rather than escaped.
function redirect_to(url)
html("Status: 302 Found\n")
html("Cache-Control: no-cache, no-store\n")
- html("Location: " .. strip_controls(url) .. "\n")
+ html("Location: " .. url:gsub("%c", "") .. "\n")
end
-- A failed login lands back on the page it came from with this flag, which
@@ -501,7 +454,7 @@ end
function authenticate_post()
local redirect = validate_value("redirect", post["redirect"])
- if redirect == nil or not is_safe_redirect(redirect) then
+ if not is_safe_redirect(redirect) then
not_found()
return 0
end
@@ -581,33 +534,17 @@ function body()
return 0
end
--- filter_open unpacks the action and request fields cgit passes in fixed
--- order, filter_write collects a post body, and filter_close runs the action.
-
local actions = {}
actions["authenticate-post"] = authenticate_post
actions["authenticate-cookie"] = authenticate_cookie
actions["body"] = body
-function filter_open(...)
- action = actions[select(1, ...)]
-
+function filter_open(name, cookie, method, query, referer, path, host, https,
+ repo, page, url, login)
+ action = actions[name]
post = {}
-
- http = {}
- http["cookie"] = select(2, ...)
- http["method"] = select(3, ...)
- http["query"] = select(4, ...)
- http["referer"] = select(5, ...)
- http["path"] = select(6, ...)
- http["host"] = select(7, ...)
- http["https"] = select(8, ...)
-
- cgit = {}
- cgit["repo"] = select(9, ...)
- cgit["page"] = select(10, ...)
- cgit["url"] = select(11, ...)
- cgit["login"] = select(12, ...)
+ http = { cookie = cookie }
+ cgit = { repo = repo, url = url, login = login }
end
function filter_close()
diff --git a/custom/extensions/auth-inline.lua b/custom/extensions/auth-inline.lua
index c4b87f6..e795717 100644
--- a/custom/extensions/auth-inline.lua
+++ b/custom/extensions/auth-inline.lua
@@ -53,7 +53,6 @@
-- still lists a protected repository with its description unless cgitrc sets
-- repo.hide=1 for it.
--- The values that follow are the configuration and are meant to be edited.
-- Protected repositories and the users allowed into each. A repository named
-- here is protected and one that is not named is public. The repository key
@@ -104,6 +103,7 @@ local cookie_insecure = false
-- the password's length, and one longer than this is turned down unchecked.
local max_password_bytes = 256
+
-- A browser accepts a cookie named __Host- only when it is Secure, covers the
-- whole host and was set by this host, so no sibling subdomain can plant a
-- session in its place. Either cookie setting above can rule that out.
@@ -112,12 +112,18 @@ if not cookie_insecure and cookie_path == "/" then
session_cookie = "__Host-" .. cookie_name
end
+-- A cookie carries its expiry and Max-Age as whole seconds, while on Lua 5.3
+-- and later a length worked out with a fraction, such as 1.5 days, is a float
+-- that tostring would write with a trailing ".0".
+session_seconds = math.floor(session_seconds)
+
-- One request reaches this script as an open, a write and a close, so what the
-- open decodes is kept here for the calls that follow.
local action, http, cgit, post
--- The two lookups below, account_hash and repo_userset, are the only part
--- of this script that differs from auth-file.lua.
+-- Apart from the configuration, the lookups below, account_hash and
+-- repo_userset with the fold that feeds them, are the only code that
+-- differs from auth-file.lua, which defines trim among its lookups.
-- The configured tables are folded to lowercased user names once, so that a
-- lookup matches whatever case the login form was filled in with, the way
@@ -141,9 +147,6 @@ end
-- a failed login can spend the same work on a hash of the same kind and
-- cost, and its timing does not tell whether the name exists.
function account_hash(user)
- if user == nil then
- return nil
- end
local hash = users[user:lower()]
if hash ~= nil then
return hash
@@ -155,31 +158,20 @@ end
-- The users allowed into a repository come back keyed by lowercased name, and
-- a repository the table does not name comes back as nil.
function repo_userset(repo)
- if repo == nil then
- return nil
- end
return protected_repos[repo]
end
--- The URL helpers below are adapted from keplerproject/wsapi.
+-- The url helpers below are adapted from keplerproject/wsapi.
function url_decode(str)
- if not str then
- return ""
- end
str = string.gsub(str, "+", " ")
str = string.gsub(str, "%%(%x%x)", function(hex)
return string.char(tonumber(hex, 16))
end)
- str = string.gsub(str, "\r\n", "\n")
return str
end
function url_encode(str)
- if not str then
- return ""
- end
- str = string.gsub(str, "\n", "\r\n")
str = string.gsub(str, "([^%w ])", function(char)
return string.format("%%%02X", string.byte(char))
end)
@@ -191,7 +183,7 @@ end
-- '=', for example a base64 password, so the value runs to the next '&'.
function parse_query(query)
local params = {}
- for key, value in string.gmatch(query or "", "([^&=]+)=([^&]*)") do
+ for key, value in string.gmatch(query, "([^&=]+)=([^&]*)") do
params[url_decode(key)] = url_decode(value)
end
return params
@@ -212,7 +204,7 @@ local function trim(text)
return text:sub(first, last)
end
--- The token comes back still URL encoded by secure_value. Decoding it here
+-- The token comes back still url encoded by secure_value. Decoding it here
-- would break the signature check for any value carrying a percent escape.
function get_cookie(cookies, name)
for raw in string.gmatch(cookies or "", "[^;]+") do
@@ -311,46 +303,9 @@ end
-- or to nil to turn it down, which is how a session meets its account's
-- current hash.
function validate_value(expected_field, cookie, bind)
- local i = 0
- local value = ""
- local field = ""
- local expiration = 0
- local salt = ""
- local signature = ""
-
- if cookie == nil or cookie:len() < 3 or cookie:sub(1, 1) == "|" then
- return nil
- end
-
- for component in string.gmatch(cookie, "[^|]+") do
- if i == 0 then
- field = component
- elseif i == 1 then
- value = component
- elseif i == 2 then
- -- The expiration must be a plain integer, since
- -- tonumber and tostring of "1e9" or "100.0" differ
- -- across Lua versions and the signed bytes have to
- -- come back byte for byte.
- if not string.match(component, "^%d+$") then
- return nil
- end
- expiration = tonumber(component)
- elseif i == 3 then
- salt = component
- elseif i == 4 then
- signature = component
- else
- break
- end
- i = i + 1
- end
-
- if signature == nil or signature:len() == 0 then
- return nil
- end
-
- if url_decode(field) ~= expected_field then
+ local field, value, expiration, salt, signature =
+ string.match(cookie or "", "^([^|]+)|([^|]+)|(%d+)|([^|]+)|([^|]+)$")
+ if signature == nil or url_decode(field) ~= expected_field then
return nil
end
@@ -369,12 +324,13 @@ function validate_value(expected_field, cookie, bind)
end
end
- local payload = field .. "|" .. value .. "|" .. tostring(expiration) .. "|" .. salt
+ local payload = field .. "|" .. value .. "|" .. expiration .. "|" .. salt
if not constant_equals(signature, sign(payload, binding)) then
return nil
end
-- An expiration of 0 never expires and is used for the redirect token.
+ expiration = tonumber(expiration)
if expiration ~= 0 and expiration <= os.time() then
return nil
end
@@ -396,12 +352,6 @@ function secure_value(field, value, expiration, binding)
return payload .. "|" .. sign(payload, binding)
end
--- A control character in a header value would let that value split the
--- response, so it is dropped rather than escaped.
-function strip_controls(s)
- return (string.gsub(s or "", "%c", ""))
-end
-
-- Compare two strings in time that does not depend on how many leading bytes
-- match, so a mismatch position is not revealed by timing.
function constant_equals(a, b)
@@ -417,7 +367,8 @@ function constant_equals(a, b)
end
-- An empty value is how a login is cleared, since Max-Age=0 tells the browser
--- to drop the cookie it already holds.
+-- to drop the cookie it already holds. Any other value is a token secure_value
+-- built, which holds no control character to split the response with.
function set_cookie(cookie, value)
local attributes = "; HttpOnly; SameSite=Lax; Path=" .. cookie_path
if not cookie_insecure then
@@ -425,16 +376,18 @@ function set_cookie(cookie, value)
end
if value == "" then
attributes = attributes .. "; Max-Age=0"
- elseif session_seconds > 0 then
+ else
attributes = attributes .. "; Max-Age=" .. tostring(session_seconds)
end
- html("Set-Cookie: " .. cookie .. "=" .. strip_controls(value) .. attributes .. "\n")
+ html("Set-Cookie: " .. cookie .. "=" .. value .. attributes .. "\n")
end
+-- A control character in the location would let it split the response, so
+-- it is dropped rather than escaped.
function redirect_to(url)
html("Status: 302 Found\n")
html("Cache-Control: no-cache, no-store\n")
- html("Location: " .. strip_controls(url) .. "\n")
+ html("Location: " .. url:gsub("%c", "") .. "\n")
end
-- A failed login lands back on the page it came from with this flag, which
@@ -462,7 +415,7 @@ end
function authenticate_post()
local redirect = validate_value("redirect", post["redirect"])
- if redirect == nil or not is_safe_redirect(redirect) then
+ if not is_safe_redirect(redirect) then
not_found()
return 0
end
@@ -542,33 +495,17 @@ function body()
return 0
end
--- filter_open unpacks the action and request fields cgit passes in fixed
--- order, filter_write collects a post body, and filter_close runs the action.
-
local actions = {}
actions["authenticate-post"] = authenticate_post
actions["authenticate-cookie"] = authenticate_cookie
actions["body"] = body
-function filter_open(...)
- action = actions[select(1, ...)]
-
+function filter_open(name, cookie, method, query, referer, path, host, https,
+ repo, page, url, login)
+ action = actions[name]
post = {}
-
- http = {}
- http["cookie"] = select(2, ...)
- http["method"] = select(3, ...)
- http["query"] = select(4, ...)
- http["referer"] = select(5, ...)
- http["path"] = select(6, ...)
- http["host"] = select(7, ...)
- http["https"] = select(8, ...)
-
- cgit = {}
- cgit["repo"] = select(9, ...)
- cgit["page"] = select(10, ...)
- cgit["url"] = select(11, ...)
- cgit["login"] = select(12, ...)
+ http = { cookie = cookie }
+ cgit = { repo = repo, url = url, login = login }
end
function filter_close()
diff --git a/custom/extensions/email-gravatar.lua b/custom/extensions/email-gravatar.lua
index aebb27c..467deff 100644
--- a/custom/extensions/email-gravatar.lua
+++ b/custom/extensions/email-gravatar.lua
@@ -47,7 +47,7 @@ local base_url = "https://gravatar.com/avatar/"
-- One name reaches this filter as an open, writes and a close, so what the
-- open works out is kept here for the close.
-local buffer = ""
+local chunks = {}
local avatar_hash = nil
local function hash_hex(input)
@@ -96,7 +96,7 @@ local function normalize_email(email)
end
function filter_open(email, page)
- buffer = ""
+ chunks = {}
local address = normalize_email(email)
if address == nil or not has_digest then
avatar_hash = nil
@@ -106,7 +106,7 @@ function filter_open(email, page)
end
function filter_write(text)
- buffer = buffer .. text
+ chunks[#chunks + 1] = text
end
local function avatar_url(size)
@@ -118,14 +118,15 @@ end
-- off the critical path. The icon only decorates the name beside it, so its
-- alt text is empty and a screen reader goes straight to the name.
function filter_close()
+ local text = table.concat(chunks)
if avatar_hash == nil then
- html(buffer)
+ html(text)
else
html("<img src='" .. avatar_url(avatar_size) ..
"' srcset='" .. avatar_url(avatar_size * 2) .. " 2x" ..
"' alt='' width='" .. avatar_size ..
"' height='" .. avatar_size ..
- "' loading='lazy' decoding='async'> " .. buffer)
+ "' loading='lazy' decoding='async'> " .. text)
end
return 0
end
diff --git a/custom/extensions/email-libravatar.lua b/custom/extensions/email-libravatar.lua
index fca67a7..0a6e10c 100644
--- a/custom/extensions/email-libravatar.lua
+++ b/custom/extensions/email-libravatar.lua
@@ -46,7 +46,7 @@ local base_url = "https://seccdn.libravatar.org/avatar/"
-- One name reaches this filter as an open, writes and a close, so what the
-- open works out is kept here for the close.
-local buffer = ""
+local chunks = {}
local avatar_hash = nil
local function hash_hex(input)
@@ -95,7 +95,7 @@ local function normalize_email(email)
end
function filter_open(email, page)
- buffer = ""
+ chunks = {}
local address = normalize_email(email)
if address == nil or not has_digest then
avatar_hash = nil
@@ -105,7 +105,7 @@ function filter_open(email, page)
end
function filter_write(text)
- buffer = buffer .. text
+ chunks[#chunks + 1] = text
end
local function avatar_url(size)
@@ -117,14 +117,15 @@ end
-- off the critical path. The icon only decorates the name beside it, so its
-- alt text is empty and a screen reader goes straight to the name.
function filter_close()
+ local text = table.concat(chunks)
if avatar_hash == nil then
- html(buffer)
+ html(text)
else
html("<img src='" .. avatar_url(avatar_size) ..
"' srcset='" .. avatar_url(avatar_size * 2) .. " 2x" ..
"' alt='' width='" .. avatar_size ..
"' height='" .. avatar_size ..
- "' loading='lazy' decoding='async'> " .. buffer)
+ "' loading='lazy' decoding='async'> " .. text)
end
return 0
end
diff --git a/custom/extensions/link-commits.lua b/custom/extensions/link-commits.lua
index 3666140..fc095a4 100644
--- a/custom/extensions/link-commits.lua
+++ b/custom/extensions/link-commits.lua
@@ -28,8 +28,8 @@ local objects = {
}
-- 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
+-- 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 wins, so put the more specific
-- patterns first, and an empty list leaves only object names linked. The
-- examples are commented out, so an unedited copy links object names alone,
@@ -54,15 +54,15 @@ local rules = {
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.
+-- 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
+-- 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)
diff --git a/custom/extensions/link-trailers.lua b/custom/extensions/link-trailers.lua
index b281150..df7c661 100644
--- a/custom/extensions/link-trailers.lua
+++ b/custom/extensions/link-trailers.lua
@@ -13,9 +13,9 @@
-- 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
+-- 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
+-- 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
@@ -38,7 +38,7 @@ local rules = {
-- 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
+-- 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
@@ -46,15 +46,15 @@ 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.
+-- 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
+-- 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)
@@ -66,7 +66,7 @@ local function make_link(url_template, capture, 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.
+-- 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
diff --git a/custom/extensions/syntax-highlight.lua b/custom/extensions/syntax-highlight.lua
index 8a1e7ab..7414b63 100644
--- a/custom/extensions/syntax-highlight.lua
+++ b/custom/extensions/syntax-highlight.lua
@@ -1,4 +1,4 @@
--- Server-side syntax highlighting for cgit's tree and blob views, named by
+-- Server-side syntax highlighting for cgit's tree and blame pages, named by
-- the source-filter setting and run inside cgit's embedded Lua interpreter
-- so a coloured blob costs no extra process per request. Tokens come from the
-- Scintillua lexers and reach the page wrapped in span elements carrying the
@@ -28,7 +28,8 @@ local scintillua_env = "CGIT_SCINTILLUA_PATH"
-- directory next to cgitrc is enough for it to be found. The lexers come
-- from https://orbitalquark.github.io/scintillua/ and need lpeg, built for
-- the Lua cgit is linked against. Forgetting that is the usual reason
--- nothing is coloured.
+-- nothing is coloured. A file is coloured only when Scintillua's detect()
+-- names its lexer, so an older Scintillua without detect() colours nothing.
--
-- # Debian and Ubuntu
-- sudo apt install lua-lpeg
@@ -74,25 +75,6 @@ local css = {
pseudoelement = "hl-func",
}
--- Extension to lexer name fixes for the fallback path, reached only when
--- this Scintillua has no detect(). Most extensions already equal their lexer
--- name and these are the frequent exceptions.
-local ext_lexer = {
- py = "python",
- js = "javascript",
- ts = "typescript",
- rb = "ruby",
- pl = "perl",
- pm = "perl",
- sh = "bash",
- md = "markdown",
- htm = "html",
- yml = "yaml",
- rs = "rust",
- c = "ansi_c",
- h = "ansi_c",
-}
-
local scintillua = nil
local filename = ""
@@ -170,37 +152,13 @@ local function load_scintillua()
return nil
end
--- Loading a lexer runs its Lua, and one written for another Scintillua can
--- raise, so a failure here just leaves this file uncoloured.
-local function load_lexer_name(name)
- if name == nil then
- return nil
- end
- local ok, lexer = pcall(scintillua.load, name)
- if ok and lexer then
- return lexer
- end
- return nil
-end
-
+-- The first line goes along to detect(), so a script with no extension is
+-- still known by its shebang. detect() matches an extension by its case, so
+-- a name in capitals such as MAIN.C is tried again in lower case.
local function lexer_for(name, text)
- if type(scintillua.detect) == "function" then
- -- The first line goes along, so a script with no extension is still
- -- known by its shebang.
- local ok, lang = pcall(scintillua.detect, name, text:match("^[^\n]*"))
- if ok and lang then
- local lexer = load_lexer_name(lang)
- if lexer then
- return lexer
- end
- end
- end
- local ext = string.match(name, "%.([^.]+)$")
- if not ext then
- return nil
- end
- ext = string.lower(ext)
- return load_lexer_name(ext_lexer[ext]) or load_lexer_name(ext)
+ local first = string.match(text, "^[^\n]*")
+ local lang = scintillua.detect(name, first) or scintillua.detect(string.lower(name), first)
+ return lang and scintillua.load(lang)
end
local function highlight(text)
@@ -208,8 +166,8 @@ local function highlight(text)
if not lexer then
return nil
end
- local ok, tokens = pcall(lexer.lex, lexer, text)
- if not ok or type(tokens) ~= "table" then
+ local tokens = lexer:lex(text)
+ if type(tokens) ~= "table" then
return nil
end
local out = {}
@@ -262,6 +220,9 @@ function filter_close()
scintillua = load_scintillua() or false
end
if scintillua then
+ -- Every step of highlight runs Scintillua's own Lua, and code
+ -- written for another version of it can raise, so a failure
+ -- anywhere there just leaves this file uncoloured.
local ok, marked = pcall(highlight, text)
if ok and marked then
html(marked)
diff --git a/custom/hooks/post-receive.cgit-age b/custom/hooks/post-receive.cgit-age
index a87e325..82a13f0 100755
--- a/custom/hooks/post-receive.cgit-age
+++ b/custom/hooks/post-receive.cgit-age
@@ -2,8 +2,13 @@
# Updates the age-file cgit reads for the idle column. The path below is the
# default age-file, so a cgitrc that changes the setting needs the same change
-# here. To install the hook, copy or link it to hooks/post-receive in each
-# repository.
+# here. A file the pusher cannot write stays as it was, and nothing here
+# complains or exits non-zero, so the pusher is not shown an error over a stale
+# idle column.
+#
+# To install the hook, copy or link it to hooks/post-receive in each
+# repository. Git runs one post-receive per repository, so fold this together
+# with the other hooks here to have more than one.
set -eu
@@ -32,10 +37,10 @@ date="$(git for-each-ref --sort=-committerdate --count=1 \
# being handed an empty file.
test -n "$date" || exit 0
-mkdir -p "$(dirname "$agefile")"
+mkdir -p "$(dirname "$agefile")" 2>/dev/null || exit 0
# Write through a temporary file so a reader never sees a half written date.
tmpfile="$agefile.$$"
trap 'rm -f "$tmpfile"' EXIT
-printf '%s\n' "$date" >"$tmpfile"
-mv "$tmpfile" "$agefile"
+{ printf '%s\n' "$date" >"$tmpfile"; } 2>/dev/null || exit 0
+mv "$tmpfile" "$agefile" 2>/dev/null || :
diff --git a/custom/hooks/post-receive.cgit-cache b/custom/hooks/post-receive.cgit-cache
index f1a4b40..61ac1ea 100755
--- a/custom/hooks/post-receive.cgit-cache
+++ b/custom/hooks/post-receive.cgit-cache
@@ -19,12 +19,12 @@
# The path below is the default cache-root, so a cgitrc that changes the
# setting needs the same change here. The web server owns the cache root, so
# the pushing user needs write access to it. Nothing here complains or exits
-# nonzero when that access is missing, so the pusher is not shown an error
+# non-zero when that access is missing, so the pusher is not shown an error
# over a stale page.
#
# To install the hook, copy or link it to hooks/post-receive in each
# repository. Git runs one post-receive per repository, so fold this together
-# with the age-file hook to have both.
+# with the other hooks here to have more than one.
set -eu
diff --git a/custom/hooks/post-receive.commit-graph b/custom/hooks/post-receive.commit-graph
index 51d8c3c..d2d711c 100755
--- a/custom/hooks/post-receive.commit-graph
+++ b/custom/hooks/post-receive.commit-graph
@@ -10,7 +10,7 @@
# --split writes each push's new commits as a small layer over the last, and
# git merges the layers as they pile up, so a push costs little. A git too old
# for these options leaves the graph as it was, and nothing here complains or
-# exits nonzero, so the pusher is not shown an error over a missing speedup.
+# exits non-zero, so the pusher is not shown an error over a missing speedup.
#
# To install the hook, copy or link it to hooks/post-receive in each
# repository. Git runs one post-receive per repository, so fold this together
diff --git a/custom/servers/apache.conf b/custom/servers/apache.conf
index 7778c7f..3d65d5e 100644
--- a/custom/servers/apache.conf
+++ b/custom/servers/apache.conf
@@ -17,16 +17,14 @@
# cgit CGI binary /usr/lib/cgit/cgit.cgi
# static assets /usr/share/cgit (cgit.css cgit.js cgit.png favicon.ico robots.txt)
# cgit config /etc/cgitrc
-# public URL https://git.example.org/ (cgit at the domain root)
+# public url https://git.example.org/ (cgit at the domain root)
#
# cgit is one CGI executable. It learns the repository and the page from
-# PATH_INFO and reads page options such as h= and id= from QUERY_STRING.
-# ScriptAlias runs the binary and forwards the trailing path as PATH_INFO, so
-# no extra path tuning is needed. cgit builds its own link base from
-# SCRIPT_NAME. The five static assets are served straight off disk. mod_alias
-# resolves Alias and ScriptAlias in order and the first match wins, so the
-# static Alias lines come before the catch-all ScriptAlias to stop the two
-# routes from shadowing each other.
+# PATH_INFO and reads page options such as h= and id= from QUERY_STRING. The
+# five static assets are served straight off disk. mod_alias resolves Alias and
+# ScriptAlias in order and the first match wins, so the static Alias lines come
+# before the catch-all ScriptAlias to stop the two routes from shadowing each
+# other.
# ServerRoot is what every relative path below resolves against, including the
@@ -138,8 +136,8 @@ AddType text/plain .txt
Require all granted
# These assets rarely change, so let browsers cache them. Keep the
- # caching scoped to this directory. Ordinary cgit pages carry no caching
- # headers, so a server-wide ExpiresDefault would stamp freshness onto
+ # caching scoped to this directory. Ordinary cgit pages carry no expiry
+ # time, so a server-wide ExpiresDefault would stamp freshness onto
# them, fight the no-store cgit puts on the login page, and could let
# one visitor's page be served to another from a shared cache.
<IfModule mod_expires.c>
@@ -150,7 +148,6 @@ AddType text/plain .txt
# The cgit binary.
<Directory "/usr/lib/cgit">
- # Allow CGI execution here.
Options +ExecCGI
# Run cgit.cgi as a CGI even if it is ever reached through a plain Alias
# rather than ScriptAlias.
@@ -204,13 +201,8 @@ AddType text/plain .txt
LimitRequestBody 65536
# Site-wide security headers are set here so they also cover the static
- # assets Apache serves. cgit itself sends only the headers the proxy
- # cannot supply. Those are Status, Content-Type, Content-Length and
- # Content-Disposition on downloads, Location on redirects, a Cache-Control
- # marking the login page no-store and a page behind an auth filter private,
- # the auth filter's Set-Cookie, and on raw repository bytes a nosniff of
- # its own next to the stricter policy "default-src 'none'". Everything
- # else, this policy included, is the proxy's job.
+ # assets Apache serves. On raw repository bytes cgit sends a nosniff and
+ # the stricter policy "default-src 'none'" of its own.
#
# The word setifempty is load bearing on the two headers cgit can also
# emit. "Header always set" replaces a same-named header even when the
@@ -248,23 +240,22 @@ AddType text/plain .txt
# once browsers have seen it.
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains"
- # These five files are the only things served off disk. cgit.css and
- # cgit.js are the paths cgit's HTML points at by default, so if you
- # relocate the assets update both these Alias targets and the css, js,
- # logo and favicon settings in cgitrc to agree.
+ # These five files are the only things served off disk. cgit's default
+ # css, js, logo and favicon settings point at these paths, so if you
+ # relocate the assets update both these Alias targets and those settings
+ # in cgitrc to agree.
Alias /cgit.css /usr/share/cgit/cgit.css
Alias /cgit.js /usr/share/cgit/cgit.js
Alias /cgit.png /usr/share/cgit/cgit.png
Alias /favicon.ico /usr/share/cgit/favicon.ico
Alias /robots.txt /usr/share/cgit/robots.txt
- # ScriptAlias maps a URL prefix to a path, marks it executable, and
- # forwards the rest of the URL as PATH_INFO. Mapping / makes cgit the
- # handler for every URL the static Aliases above did not already claim.
- # The trailing slash on cgit.cgi/ is load bearing, telling Apache that
- # cgit.cgi is the program and the rest of the URL is PATH_INFO. cgit
- # derives its link base from SCRIPT_NAME, which at the domain root is /
- # and needs no tuning. For a sub-path install see the note below.
+ # Mapping / makes cgit the handler for every url the static Aliases above
+ # did not already claim. The trailing slash on cgit.cgi/ is load bearing,
+ # telling Apache that cgit.cgi is the program and the rest of the url is
+ # PATH_INFO. cgit derives its link base from SCRIPT_NAME, which at the
+ # domain root is / and needs no tuning. For a sub-path install see the
+ # note below.
ScriptAlias / /usr/lib/cgit/cgit.cgi/
</VirtualHost>
diff --git a/custom/servers/lighttpd.conf b/custom/servers/lighttpd.conf
index 8fef3c4..edbf162 100644
--- a/custom/servers/lighttpd.conf
+++ b/custom/servers/lighttpd.conf
@@ -9,19 +9,18 @@
# lighttpd -D -f /path/to/lighttpd.conf # run it in the foreground
#
# lighttpd runs the cgit.cgi binary directly through mod_cgi, so no FastCGI
-# bridge is needed. This is cgit's classic reference deployment, mod_cgi with
-# mod_alias and mod_setenv.
+# bridge is needed.
#
# Paths assumed below, edit them to match your install.
# cgit CGI binary /usr/lib/cgit/cgit.cgi
# static assets /usr/share/cgit (cgit.css cgit.js cgit.png favicon.ico robots.txt)
# cgit config /etc/cgitrc
-# public URL https://git.example.org/ (cgit at the domain root)
+# public url https://git.example.org/ (cgit at the domain root)
#
# cgit is one CGI executable. It learns the repository and the page from
-# PATH_INFO and reads page options such as h= and id= from QUERY_STRING. cgit
-# builds its own link base from SCRIPT_NAME. The five static assets are served
-# straight off disk and must never be routed through cgit.
+# PATH_INFO and reads page options such as h= and id= from QUERY_STRING. The
+# five static assets are served straight off disk and must never be routed
+# through cgit.
# A plain assignment rather than "+=", since this config stands on its own
@@ -75,13 +74,8 @@ $HTTP["host"] == "git.example.org" {
setenv.add-environment = ( "CGIT_CONFIG" => "/etc/cgitrc" )
# Site-wide security headers are set here so they also cover the static
- # assets lighttpd serves. cgit itself sends only the headers the server
- # cannot supply. Those are Status, Content-Type, Content-Length and
- # Content-Disposition on downloads, Location on redirects, a Cache-Control
- # marking the login page no-store and a page behind an auth filter private,
- # the auth filter's Set-Cookie, and on raw repository bytes a nosniff of
- # its own next to the stricter policy "default-src 'none'". Everything
- # else, this policy included, is the server's job.
+ # assets lighttpd serves. On raw repository bytes cgit sends a nosniff and
+ # the stricter policy "default-src 'none'" of its own.
#
# The add in add-response-header is load bearing. It appends a second
# copy next to what cgit emitted, so a raw page carries both policies and
@@ -128,7 +122,7 @@ $HTTP["host"] == "git.example.org" {
# hands everything else to cgit.
#
# The trailing slash on cgit.cgi/ is load bearing. lighttpd builds the
- # physical path by stripping the matched key off the front of the URL and
+ # physical path by stripping the matched key off the front of the url and
# appending the rest to the value. For the key / the remainder carries no
# leading slash, so without the trailing slash a request for
# /linux/tree/kernel/sched.c glues onto the binary name as
diff --git a/custom/servers/nginx.conf b/custom/servers/nginx.conf
index d18a109..7f09682 100644
--- a/custom/servers/nginx.conf
+++ b/custom/servers/nginx.conf
@@ -12,13 +12,12 @@
# cgit CGI binary /usr/lib/cgit/cgit.cgi
# static assets /usr/share/cgit (cgit.css cgit.js cgit.png favicon.ico robots.txt)
# cgit config /etc/cgitrc
-# public URL https://git.example.org/ (cgit at the domain root)
+# public url https://git.example.org/ (cgit at the domain root)
#
# cgit is one CGI executable. It learns the repository and the page from
# PATH_INFO and reads page options such as h= and id= from QUERY_STRING, so
-# nginx must pass PATH_INFO through to the binary. cgit builds its own link
-# base from SCRIPT_NAME. The five static assets are served straight off disk
-# and must never be routed through cgit.
+# nginx must pass PATH_INFO through to the binary. The five static assets are
+# served straight off disk and must never be routed through cgit.
#
# nginx cannot run CGI programs itself, so a small bridge called fcgiwrap runs
# the cgit.cgi binary and speaks FastCGI to nginx. Nothing below works until
@@ -120,7 +119,6 @@ http {
root /var/www/html;
}
- # Everything else moves to HTTPS.
location / {
return 301 https://$host$request_uri;
}
@@ -152,15 +150,9 @@ http {
ssl_session_timeout 1d;
ssl_session_tickets off;
- # Site-wide security headers sit here because they must also cover the
- # static assets nginx serves directly. cgit itself sends only the
- # headers the proxy cannot supply. Those are Status, Content-Type,
- # Content-Length and Content-Disposition on downloads, Location on
- # redirects, a Cache-Control marking the login page no-store and a page
- # behind an auth filter private, the auth filter's Set-Cookie, and on
- # raw repository bytes a nosniff of its own next to the stricter policy
- # "default-src 'none'". Everything else, this policy included, is the
- # proxy's job.
+ # Site-wide security headers are set here so they also cover the
+ # static assets nginx serves. On raw repository bytes cgit sends a
+ # nosniff and the stricter policy "default-src 'none'" of its own.
#
# add_header appends and never replaces what cgit sent, so a raw page
# carries both policies and the browser enforces the stricter one,
@@ -199,9 +191,9 @@ http {
# hard to undo once browsers have seen it.
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
- # The document root is the directory that holds the static assets. cgit
- # emits absolute links to /cgit.css and /cgit.png by default, so those
- # files must resolve at the root of the URL space.
+ # The document root is the directory that holds the static assets.
+ # cgit's default css, js, logo and favicon settings are absolute links
+ # to these files, so they must resolve at the root of the url space.
root /usr/share/cgit;
# The only request body cgit ever reads is the auth-filter login form,
@@ -214,18 +206,17 @@ http {
# Match the assets by their exact root-level names, never by bare
# extension. A repository can hold files ending in .css or .png, and
- # /myrepo/tree/style.css is a real cgit URL a broad match would
+ # /myrepo/tree/style.css is a real cgit url a broad match would
# capture and 404. The regex anchored at ^/ matches /cgit.css but
# never /myrepo/tree/cgit.css, and a regex location beats the prefix
- # location below, so assets win their exact URLs and cgit the rest.
+ # location below, so assets win their exact urls and cgit the rest.
location ~ ^/(cgit\.css|cgit\.js|cgit\.png|favicon\.ico|robots\.txt)$ {
expires 30d;
access_log off;
try_files $uri =404;
}
- # Everything that is not a static asset above is a cgit URL, the repo
- # index, a repository, a page within a repository, a snapshot, a feed.
+ # Everything else is a cgit url.
location / {
# The CGI environment, normally "include fastcgi_params", written
# out here so this config stands alone. The cookie and referer
@@ -249,7 +240,7 @@ http {
# whole request path is the PATH_INFO.
fastcgi_param PATH_INFO $uri;
- # Page options such as h=branch, id=sha and the snapshot format.
+ # Page options such as h=branch and id=sha.
fastcgi_param QUERY_STRING $query_string;
# Which config cgit reads. It checks CGIT_CONFIG and falls back to
@@ -257,11 +248,11 @@ http {
# explicit and lets you move cgitrc without recompiling.
fastcgi_param CGIT_CONFIG /etc/cgitrc;
- # The browser's Host header, so cgit builds clone URLs against the
+ # The browser's Host header, so cgit builds clone urls against the
# name the visitor used rather than server_name.
fastcgi_param HTTP_HOST $http_host;
- # The real scheme, so cgit builds correct https clone URLs.
+ # The real scheme, so cgit builds correct https clone urls.
fastcgi_param HTTPS $https if_not_empty;
# The routine remainder, needed by fcgiwrap and by the login form.
@@ -281,7 +272,6 @@ http {
fastcgi_param SERVER_PORT $server_port;
fastcgi_param SERVER_NAME $server_name;
- # Hand off to the fcgiwrap socket.
fastcgi_pass unix:/run/fcgiwrap.socket;
# Large outputs such as snapshot tarballs and blame on big files
@@ -289,7 +279,7 @@ http {
fastcgi_read_timeout 300s;
fastcgi_buffering off;
- # Ordinary cgit pages carry no caching headers, so browsers
+ # Ordinary cgit pages carry no expiry time, so browsers
# refetch them and cgit's internal cache keeps that cheap. Do not
# add a blanket expires or Cache-Control in this location. It
# would fight the no-store cgit puts on the login page and could
@@ -300,7 +290,7 @@ http {
# To serve cgit at https://git.example.org/cgit/ instead of the root, split
-# the URL so SCRIPT_NAME is the prefix and PATH_INFO is the rest. Keep every
+# the url so SCRIPT_NAME is the prefix and PATH_INFO is the rest. Keep every
# other fastcgi_param from the location above.
#
# location /cgit/ {
@@ -320,7 +310,7 @@ http {
# access_log off;
# }
#
-# cgit's default css=/cgit.css and logo=/cgit.png point at the domain root, so
-# under a sub-path also set css=/cgit/cgit.css and logo=/cgit/cgit.png in
-# cgitrc. cgit derives the /cgit prefix from SCRIPT_NAME. If links come out
-# wrong, pin it in cgitrc with virtual-root=/cgit/.
+# cgit's default css, js, logo and favicon settings point at the domain root,
+# so under a sub-path also set them to the /cgit/ paths in cgitrc. cgit
+# derives the /cgit prefix from SCRIPT_NAME. If links come out wrong, pin it
+# in cgitrc with virtual-root=/cgit/.