diff options
Diffstat (limited to '')
-rw-r--r--CHANGELOG.txt152
-rw-r--r--MANUAL.txt8
-rw-r--r--Makefile2
-rw-r--r--README.txt3
-rw-r--r--assets/cgit.css25
-rw-r--r--assets/cgit.js18
-rw-r--r--custom/extensions/syntax-highlight.lua45
-rw-r--r--source/cgit.c2
-rw-r--r--source/cgit.h2
-rw-r--r--source/html.c79
-rw-r--r--source/html.h13
-rw-r--r--source/ui-blame.c18
-rw-r--r--source/ui-patch.c12
-rw-r--r--source/ui-tree.c4
-rw-r--r--tests/extensions/test-syntax-highlight.lua29
-rwxr-xr-xtests/t0005-markup.sh23
-rwxr-xr-xtests/t0104-tree.sh60
-rwxr-xr-xtests/t0109-patch.sh10
-rwxr-xr-xtests/t0502-syntax-highlight.sh11
19 files changed, 486 insertions, 30 deletions
diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index 67b2670..f1b5ae1 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -5,6 +5,158 @@ Notable changes to this fork, newest release first. History before v2.0.0
belongs to upstream cgit and is not covered here.
+v2.9.1 (2026-10-24)
+-------------------
+
+A bugfix release for the tree and blame pages, whose line numbers had drifted
+away from the code since v2.0.0 in Safari and in every browser on a typical
+Linux desktop, until the code sat dozens of lines below its number at the end
+of a long file.
+
+Added
+.....
+
+* A character a browser would start a new line at, though git does not, is
+ shown as an escape sequence in span.escape on the tree and blame pages and
+ by the syntax highlighter: a carriage return with no line feed after it, a
+ vertical tab, a form feed, next line and the Unicode line and paragraph
+ separators.
+
+Changed
+.......
+
+* Patches end with the cgit version again, written as git writes its own, so
+ this release signs them "cgit 2.9.1". The footer and the generator tag still
+ carry no version.
+
+Fixed
+.....
+
+* The code beside the line numbers was set in the browser's own monospace
+ font, which made each of its lines a little taller than its number, by a
+ pixel in Safari and on Linux, so the two columns drifted apart down every
+ file.
+* A lone carriage return started an extra line in every browser, and U+2028
+ and U+2029 did in Safari, each pushing every later line below its number.
+* The line highlight sat a pixel low, covered only the digits of each number,
+ and ended at the edge of the view on a file scrolled sideways.
+* Code in a readme took the browser's own monospace font, Courier in Chrome
+ and Safari, rather than the theme's --font-mono.
+
+
+v2.9.0 (2026-10-17)
+-------------------
+
+A compatibility and hardening release. Lua 5.5 is supported, readmes render
+closer to how GitHub shows them, the auth examples were overhauled, 32-bit
+builds compile again, and two audits of the extensions and of the C code
+closed what they found, from a cache lock that an exec filter let go of to
+a log search that was never the literal match it claimed to be.
+
+Added
+.....
+
+* Lua 5.5, detected when building and claimed by every extension, which
+ needs luaossl 20260910 or later there.
+* Stable markup hooks for site themes: a data-page attribute, named sections
+ and a class per link kind.
+* Readmes follow GitHub more closely: relative links lead into the tree at
+ the readme's own ref, lines of a paragraph run together, a table may cut a
+ paragraph short, a wrapped list item stays in its list, urls and addresses
+ in angle brackets link, backslash escapes and character references are
+ honoured, underscores inside words are left alone, and headings get the
+ anchors GitHub gives them. Images and badges are left out.
+* The auth examples bind a session to the account's password hash, so a
+ changed password ends it, use a __Host- cookie where one can be set, turn
+ down passwords past 256 bytes unchecked, spend a missing account's login on
+ a real hash, load their libraries only when a request needs one, and say
+ which hash kinds each system's crypt checks.
+* Avatars are looked up by SHA-256, offered at twice the size to dense
+ screens, marked decorative, and left out when luaossl is missing.
+* The syntax highlighter finds a script's lexer from its shebang line.
+* make get-git checks the Git tarball against the SHA-256 kernel.org lists.
+* The dumb transport answers HEAD from the ref store and the index dates a
+ repository from its reftable, so reftable repositories clone and age.
+
+Changed
+.......
+
+* enable-trailers is now enable-commit-trailers.
+* The bundled Git is 2.56.0.
+* The pages carry no version number, and the Lua-less build is checked by
+ the suite as well.
+* Any path below the about page answers 404, the about filter is handed the
+ tree url its links need, and the about redirect keeps the query string.
+* The dumb transport serves only what a clone asks for, a loose object, a
+ pack with its index and info/http-alternates, sends each file with its
+ size and keeps none of them in the cache.
+* A page behind an auth filter is marked private, and every page that
+ follows a branch takes cache-dynamic-ttl.
+* summary-branches, summary-tags and max-ref-count read a negative value as
+ no limit, and the feed refuses pathspec magic as the log does.
+* patch, rawdiff and signature downloads hold to max-blob-size, and a
+ readme past it says so on the about page.
+* Diffs no longer ask xdiff for a minimal result, which cost quadratic time
+ on a reordered file, and tar.xz snapshots are compressed on one thread.
+* The cache slot lock is an flock, which outlasts an exec filter's swap of
+ stdout where the record lock did not.
+* Git is built without translations or Tcl, the release script builds with
+ the compiler it probed, the two builds no longer share vendor/git at once,
+ sparse checks the sources as gnu17, and a musl build is documented.
+* The example tracker links in link-commits.lua and link-trailers.lua ship
+ commented out, so an unedited copy links object names alone.
+* The manual, README and sample cgitrc say what the code does: every page
+ and query parameter, every file make install puts in place, the defaults
+ and value syntax, and which filters get the repository variables.
+
+Fixed
+.....
+
+* 32-bit builds, where Git 2.55's size_t object sizes no longer compiled.
+* The log search matched as a regular expression after all, so a term with
+ a bracket in it answered a 500 and a crafted one took the memory of the
+ box.
+* A pushed date or zone outside what git's date code accepts, and a notes
+ ref pointing at anything but a tree, ended the summary, refs, log, commit
+ and feed pages of the repository.
+* A snapshot name led to a branch spelled like the tag's stem, members
+ of a snapshot named by its suffix alone started at a slash, and a missing
+ PATH refused every compressed snapshot.
+* The plain view answered twice for duplicate tree entries and read a freed
+ mimetype on the way, a control byte in a file name made the web server
+ refuse the download, a scanned defbranch could name a path outside the
+ repository, and the error page resolved the very id it had refused.
+* A fill whose last flush failed to write, on a full disk for one, left the
+ visitor a blank response, a cache ttl in the tens of millions of minutes
+ overflowed, and an empty list reached qsort as a null, which glibc's
+ sanitizer build aborted on.
+* Links built from the repository url and the feed link's branch were not
+ encoded, a readme vanished behind an annotated tag, a followed log lost
+ its root commit and paged by the wrong name, a Lua build crashed when git
+ wrote before cgit's own setup, text was cut inside a multibyte character,
+ and the tag page offered snapshots of trees and blobs.
+* An absolute symlink target was linked as a path inside the tree, a chain
+ of single directories ended in a dangling separator, a repository without
+ a path was dated from a file named "(null)", a NUL ended a config comment
+ early, and a tree row was left open when a link target could not be read.
+* An empty repo.url is ignored, a readme ending in a colon is gated like a
+ path on disk, an error mid capture keeps its message, date-format=
+ local-local renders, an indented comment in the mimetype file is skipped,
+ types that only spell xml get no charset, the sendfile loop stops at a
+ short slot, slot keys and pages are read and written in full, and the
+ stats page stops after its error page.
+* auth-file.lua refuses every repository when it cannot read its repos file,
+ cookie, readme and avatar parsing run in linear time, the cache hook keeps
+ its rm errors from pushers, the age hook leaves its directory writable to
+ the group, a Lua script may write while it loads, and every Lua filter
+ failure is named in the log.
+* Git read the global config for its trace settings before cgit isolated
+ the environment, so every request probed $HOME/.gitconfig, which the
+ home-access test caught only once it ran on a real x86_64 kernel.
+* make DEVELOPER=1 passes: string literals sit behind const pointers and
+ callbacks are passed by address.
+
+
v2.8.0 (2026-09-16)
-------------------
diff --git a/MANUAL.txt b/MANUAL.txt
index 8060528..59e4b04 100644
--- a/MANUAL.txt
+++ b/MANUAL.txt
@@ -861,6 +861,14 @@ source filter::
be filtered is available on standard input and the filtered contents is
expected on standard output.
+ The line numbers beside the output are counted from the line feeds of
+ the input, so the output has to break lines exactly where those fall.
+ A browser also starts a line at a carriage return with no line feed
+ after it, and may at a vertical tab, a form feed, or the Unicode next
+ line, line separator and paragraph separator, which is why cgit and
+ syntax-highlight.lua show each of those as an escape sequence in
+ span.escape.
+
trailer filter::
This filter is given two parameters, the key of the trailer being
formatted, such as "Fixes", and a string indicating the originating
diff --git a/Makefile b/Makefile
index 2564bcc..3cda721 100644
--- a/Makefile
+++ b/Makefile
@@ -35,7 +35,7 @@ GIT_TARBALL = git-$(GIT_VERSION).tar.xz
# Compiled-in cgit defaults, all overridable through cgit.conf. Every one
# carries the CGIT_ prefix except CACHE_ROOT, which is inherited from upstream
# under that name.
-CGIT_VERSION = v2.8.0
+CGIT_VERSION = v2.9.1
CGIT_SCRIPT_NAME = cgit.cgi
CGIT_SCRIPT_PATH = /var/www/htdocs/cgit
CGIT_DATA_PATH = $(CGIT_SCRIPT_PATH)
diff --git a/README.txt b/README.txt
index 998c072..ceba98c 100644
--- a/README.txt
+++ b/README.txt
@@ -98,6 +98,9 @@ script added through the css and js options has stable hooks to work with.
* Relative ages are time elements with an age-* class and the timestamp in
data-ut, while a date shown in full is a plain time element. Line numbers are
anchors named n followed by the line, while enable-tree-linenumbers is on.
+ A character in a file that a browser would start a new line at, though git
+ does not, such as a carriage return with no line feed after it, is shown as
+ an escape sequence in span.escape.
* Colours, fonts and metrics are custom properties on div#cgit, so a theme can
redefine those alone. Every rule in cgit.css starts with div#cgit, apart from
the background and margins it sets on html.cgit-standalone, the class of the
diff --git a/assets/cgit.css b/assets/cgit.css
index dbd807c..540480c 100644
--- a/assets/cgit.css
+++ b/assets/cgit.css
@@ -127,6 +127,13 @@ div#cgit table {
border-collapse: collapse;
}
+div#cgit code,
+div#cgit kbd,
+div#cgit pre,
+div#cgit samp {
+ font-family: var(--font-mono);
+}
+
div#cgit select,
div#cgit input {
font-family: inherit;
@@ -448,7 +455,6 @@ div#cgit div#about pre {
div#cgit pre.plaintext {
white-space: pre-wrap;
overflow-wrap: anywhere;
- font-family: var(--font-mono);
line-height: 1.5;
margin: 0;
}
@@ -642,7 +648,20 @@ div#cgit table.blob td.linenumbers {
div#cgit table.blob pre {
padding: 0;
margin: 0;
- font-family: var(--font-mono);
+}
+
+/*
+ * The code is set exactly like the line numbers beside it, since in another
+ * font or size each line can come out taller than its number.
+ */
+div#cgit table.blob code {
+ font: inherit;
+}
+
+div#cgit span.escape {
+ color: var(--muted);
+ outline: 1px dashed var(--border-mid);
+ outline-offset: -1px;
}
div#cgit table.blob td.linenumbers a,
@@ -676,7 +695,6 @@ div#cgit table.blame {
div#cgit div.line-hl {
position: absolute;
left: 0;
- right: 0;
background: var(--line-hl);
pointer-events: none;
}
@@ -744,7 +762,6 @@ div#cgit table.blame td.lines > div > pre {
padding: 0 0 0 0.5em;
position: absolute;
top: 0;
- font-family: var(--font-mono);
}
div#cgit table.blame .oid {
diff --git a/assets/cgit.js b/assets/cgit.js
index 2c0a828..807f15f 100644
--- a/assets/cgit.js
+++ b/assets/cgit.js
@@ -112,7 +112,7 @@ var bar = null;
function place_bar() {
var match, first, last, table;
- var table_box, first_box, last_box, top, bottom;
+ var table_box, first_box, last_box, leading, origin, top, bottom;
if (bar) {
bar.remove();
@@ -131,12 +131,24 @@ function place_bar() {
table_box = table.getBoundingClientRect();
first_box = first.getBoundingClientRect();
last_box = last.getBoundingClientRect();
- top = Math.min(first_box.top, last_box.top) - table_box.top;
- bottom = Math.max(first_box.bottom, last_box.bottom) - table_box.top;
+
+ // An anchor's box holds only its digits, centred in a line that the line
+ // height makes taller, so the bar reaches out by the difference to cover
+ // whole lines. A line height of normal leaves nothing to add.
+ leading = parseFloat(getComputedStyle(first).lineHeight);
+ leading = (leading - first_box.height) / 2 || 0;
+
+ // The bar is placed from inside the table's border, not from its edge.
+ origin = table_box.top + table.clientTop;
+ top = Math.min(first_box.top, last_box.top) - leading - origin;
+ bottom = Math.max(first_box.bottom, last_box.bottom) + leading - origin;
bar = document.createElement("div");
bar.className = "line-hl";
bar.style.top = top + "px";
bar.style.height = (bottom - top) + "px";
+ // A table too wide for the page scrolls sideways, and the bar has to run
+ // to the end of its longest line rather than to the edge of the view.
+ bar.style.width = table.scrollWidth + "px";
table.appendChild(bar);
// A browser only scrolls to a fragment that names a real id, which a range
diff --git a/custom/extensions/syntax-highlight.lua b/custom/extensions/syntax-highlight.lua
index fdc4fa0..c178f25 100644
--- a/custom/extensions/syntax-highlight.lua
+++ b/custom/extensions/syntax-highlight.lua
@@ -95,10 +95,32 @@ local scintillua = nil
local filename = ""
local chunks = {}
-local escape_map = { ["&"] = "&amp;", ["<"] = "&lt;", [">"] = "&gt;" }
+-- Besides the markup characters, everything a browser may start a new line
+-- at is shown as an escape sequence, since the line numbers beside the code
+-- count only line feeds. cgit escapes a blob it serves unhighlighted the same
+-- way.
+local escape_map = {
+ ["&"] = "&amp;",
+ ["<"] = "&lt;",
+ [">"] = "&gt;",
+ ["\r"] = "<span class='escape'>\\r</span>",
+ ["\v"] = "<span class='escape'>\\v</span>",
+ ["\f"] = "<span class='escape'>\\f</span>",
+ ["\194\133"] = "<span class='escape'>\\u0085</span>",
+ ["\226\128\168"] = "<span class='escape'>\\u2028</span>",
+ ["\226\128\169"] = "<span class='escape'>\\u2029</span>",
+}
local function escape(s)
- return (string.gsub(s, "[&<>]", escape_map))
+ return (string.gsub(s, "[&<>\r\v\f]", escape_map))
+end
+
+-- The next line and the two Unicode separators take several bytes, which a
+-- lexer may hand back in different tokens, so they are escaped once the
+-- tokens are joined again.
+local function escape_separators(s)
+ s = string.gsub(s, "\194\133", escape_map)
+ return (string.gsub(s, "\226\128[\168\169]", escape_map))
end
local function scintillua_path()
@@ -208,7 +230,7 @@ local function highlight(text)
if pos <= #text then
out[#out + 1] = escape(string.sub(text, pos))
end
- return table.concat(out)
+ return escape_separators(table.concat(out))
end
function filter_open(name)
@@ -226,8 +248,13 @@ end
-- source.
function filter_close()
local text = table.concat(chunks)
+ local fits = #text <= max_bytes
chunks = {}
- if #text <= max_bytes then
+ -- A browser reads CR LF as one line feed only while the two stay
+ -- together, and a lexer could hand them back in different tokens.
+ -- Every carriage return left after this is shown by escape().
+ text = string.gsub(text, "\r\n", "\n")
+ if fits then
if scintillua == nil then
scintillua = load_scintillua() or false
end
@@ -241,8 +268,14 @@ function filter_close()
end
local pos = 1
while pos <= #text do
- html(escape(string.sub(text, pos, pos + slice_bytes - 1)))
- pos = pos + slice_bytes
+ local stop = pos + slice_bytes - 1
+ -- A slice runs on to the end of the character it would cut, so
+ -- no separator is split where escape_separators cannot see it.
+ while string.find(text, "^[\128-\191]", stop + 1) do
+ stop = stop + 1
+ end
+ html(escape_separators(escape(string.sub(text, pos, stop))))
+ pos = stop + 1
end
return 0
end
diff --git a/source/cgit.c b/source/cgit.c
index 5f8ecfa..a877e92 100644
--- a/source/cgit.c
+++ b/source/cgit.c
@@ -54,6 +54,8 @@ struct refmatch {
int found;
};
+const char *cgit_version = CGIT_VERSION;
+
/*
* Isolate git from the calling user's configuration, which could otherwise
* break once HOME is unset below. It runs as a constructor because git reads
diff --git a/source/cgit.h b/source/cgit.h
index 5b92632..c04193f 100644
--- a/source/cgit.h
+++ b/source/cgit.h
@@ -324,6 +324,8 @@ struct cgit_context {
int empty_repo;
};
+extern const char *cgit_version;
+
extern struct cgit_repolist cgit_repolist;
extern struct cgit_context ctx;
diff --git a/source/html.c b/source/html.c
index d1e28f8..d13ac5b 100644
--- a/source/html.c
+++ b/source/html.c
@@ -258,6 +258,85 @@ ssize_t html_ntxt(const char *txt, size_t len)
return left;
}
+/*
+ * git ends a line only at a line feed. A browser also starts one at a carriage
+ * return with no line feed after it, since the HTML parser turns that into a
+ * line feed, and may at the other characters Unicode makes mandatory breaks.
+ * Escaping those keeps one line on the page for every line git counts, so the
+ * numbers beside the text stay in step with it.
+ */
+const char *html_line_break_escape(const char *p, const char *end, size_t *width)
+{
+ const char *esc;
+
+ switch ((unsigned char)*p) {
+ case '\r':
+ // The parser reads CR LF as the single line feed git ends the
+ // line with.
+ if (p + 1 < end && p[1] == '\n')
+ return NULL;
+ *width = 1;
+ return "\\r";
+ case '\v':
+ *width = 1;
+ return "\\v";
+ case '\f':
+ *width = 1;
+ return "\\f";
+ case 0xc2:
+ if (end - p < 2 || memcmp(p, "\xc2\x85", 2))
+ return NULL;
+ *width = 2;
+ return "\\u0085";
+ case 0xe2:
+ if (end - p >= 3 && !memcmp(p, "\xe2\x80\xa8", 3))
+ esc = "\\u2028";
+ else if (end - p >= 3 && !memcmp(p, "\xe2\x80\xa9", 3))
+ esc = "\\u2029";
+ else
+ return NULL;
+ *width = 3;
+ return esc;
+ default:
+ return NULL;
+ }
+}
+
+void html_blob_txt(const char *txt, size_t len)
+{
+ const char *end = txt + len;
+ const char *p;
+
+ for (p = txt; p < end; p++) {
+ const char *entity = NULL, *esc = NULL;
+ size_t width = 1;
+
+ if (*p == '<')
+ entity = "&lt;";
+ else if (*p == '>')
+ entity = "&gt;";
+ else if (*p == '&')
+ entity = "&amp;";
+ else
+ esc = html_line_break_escape(p, end, &width);
+ if (!entity && !esc)
+ continue;
+ html_raw(txt, p - txt);
+ if (entity) {
+ html(entity);
+ } else {
+ // The span lets a stylesheet tell the escape from the
+ // same characters typed into the file.
+ html("<span class='escape'>");
+ html(esc);
+ html("</span>");
+ }
+ p += width - 1;
+ txt = p + 1;
+ }
+ html_raw(txt, end - txt);
+}
+
void html_attrf(const char *format, ...)
{
va_list args;
diff --git a/source/html.h b/source/html.h
index c6c0e19..9cad3a6 100644
--- a/source/html.h
+++ b/source/html.h
@@ -61,6 +61,19 @@ extern void html_txt(const char *txt);
*/
extern ssize_t html_ntxt(const char *txt, size_t len);
+/*
+ * The escape sequence a blob's text shows for the character at p when a
+ * browser would start a new line at it though git does not, or NULL for any
+ * other character. The character's length in bytes is stored in width.
+ */
+extern const char *html_line_break_escape(const char *p, const char *end, size_t *width);
+
+/*
+ * Escape the text of a blob for the page beside its line numbers, writing
+ * each character html_line_break_escape names as its escape sequence.
+ */
+extern void html_blob_txt(const char *txt, size_t len);
+
extern void html_attr(const char *txt);
extern void html_url_path(const char *txt);
diff --git a/source/ui-blame.c b/source/ui-blame.c
index 8fd6c83..f60020c 100644
--- a/source/ui-blame.c
+++ b/source/ui-blame.c
@@ -211,6 +211,15 @@ static size_t line_width(struct blame_scoreboard *sb, int line)
size_t width = 0;
while (pos < end) {
+ size_t len;
+ const char *esc = html_line_break_escape(pos, end, &len);
+
+ // An escaped character takes the room of its escape sequence.
+ if (esc) {
+ width += strlen(esc);
+ pos += len;
+ continue;
+ }
width++;
if (*pos++ == '\t')
width = (width + TAB_WIDTH - 1) & ~(TAB_WIDTH - 1);
@@ -297,9 +306,10 @@ static void print_blame_page(const struct object_id *oid, const char *path,
cgit_tree_link("tree", NULL, NULL, ctx.qry.head, rev, path);
html(")</div>\n");
- // A NUL past the window buffer_is_binary sniffs would end html_txt
- // early while the hash and line number columns still cover the whole
- // file, so a blob holding one anywhere is treated as binary too.
+ // A NUL past the window buffer_is_binary sniffs would end a Lua
+ // filter's output early while the hash and line number columns still
+ // cover the whole file, so a blob holding one anywhere is treated as
+ // binary too.
if (buffer_is_binary(buf, size) || memchr(buf, 0, size)) {
struct blame_entry *ent = sb.ent, *next;
@@ -334,7 +344,7 @@ static void print_blame_page(const struct object_id *oid, const char *path,
cgit_close_filter(ctx.repo->source_filter);
free(filter_arg);
} else {
- html_txt(buf);
+ html_blob_txt(buf, size);
}
html("</code></pre>");
diff --git a/source/ui-patch.c b/source/ui-patch.c
index 525e660..78c850c 100644
--- a/source/ui-patch.c
+++ b/source/ui-patch.c
@@ -62,6 +62,7 @@ void cgit_print_patch(const char *new_rev, const char *old_rev, const char *pref
struct commit *commit;
struct object_id new_oid, old_oid;
char rev_range[REV_RANGE_LEN];
+ const char *version;
// setup_revisions reads these the way git reads a command line, so the
// first entry stands in for the program name and is skipped, and the
// array has to stay null terminated because the path after the double
@@ -130,11 +131,16 @@ void cgit_print_patch(const char *new_rev, const char *old_rev, const char *pref
ctx.page.filename = cgit_fmt("%s.patch", rev_range);
cgit_print_http_headers();
+ // git format-patch signs with its bare version number, so the v that
+ // cgit's tags carry is left off to read the same way.
+ if (!skip_prefix(cgit_version, "v", &version))
+ version = cgit_version;
+
while ((commit = get_revision(&rev))) {
log_tree_commit(&rev, commit);
// Two dashes and a space is the mail signature separator, so
- // git am and mail readers drop the line below rather than
- // carrying it into the commit message.
- printf("-- \ncgit\n\n");
+ // git am and mail readers drop the version note below rather
+ // than carrying it into the commit message.
+ printf("-- \ncgit %s\n\n", version);
}
}
diff --git a/source/ui-tree.c b/source/ui-tree.c
index 13a9787..1d8bb3d 100644
--- a/source/ui-tree.c
+++ b/source/ui-tree.c
@@ -113,7 +113,7 @@ static void print_text_buffer(const char *filename, char *buf, size_t size)
}
html("<td class='lines'><pre><code>");
- html_txt(buf);
+ html_blob_txt(buf, size);
html("</code></pre></td></tr></table>\n");
}
@@ -188,7 +188,7 @@ static bool print_object(const struct object_id *oid, const char *path,
return false;
}
// buffer_is_binary only sniffs the front of the blob, and a NUL past
- // that window would end the escaped text render early while the line
+ // that window would end a Lua filter's output early while the line
// number column still counts the whole file, so check the rest too.
is_binary = buffer_is_binary(buf, size) || memchr(buf, 0, size);
diff --git a/tests/extensions/test-syntax-highlight.lua b/tests/extensions/test-syntax-highlight.lua
index a87cd80..f831b01 100644
--- a/tests/extensions/test-syntax-highlight.lua
+++ b/tests/extensions/test-syntax-highlight.lua
@@ -33,6 +33,17 @@ if mode == "nolexers" then
out = render("big.fake", string.rep("x<y>", 30000))
h.equals("slicing loses nothing at the boundaries", out, string.rep("x&lt;y&gt;", 30000))
+ out = render("hello.fake", "a\rb\vc\fd\r\ne")
+ h.equals("the fallback escapes what a browser breaks at", out,
+ "a<span class='escape'>\\r</span>b<span class='escape'>\\v</span>c" ..
+ "<span class='escape'>\\f</span>d\ne")
+
+ -- The line separator starts on the last byte of the first slice, and
+ -- its other two bytes would land in the next one.
+ out = render("big.fake", string.rep("x", 64 * 1024 - 1) .. "\226\128\168y")
+ h.equals("a separator across a slice boundary is still escaped", out,
+ string.rep("x", 64 * 1024 - 1) .. "<span class='escape'>\\u2028</span>y")
+
h.finish()
end
@@ -70,6 +81,24 @@ h.equals("an extension with no lexer serves escaped text", out, "import &lt;x&gt
out = render("noextension", "words <here>")
h.equals("a file with no extension serves escaped text", out, "words &lt;here&gt;")
+-- The line numbers beside the code count line feeds alone, so each character
+-- a browser would start a line at is an escape, and CR LF reaches the page as
+-- the line feed a browser would have made of it.
+out = render("hello.fake", "if\r42\vzz\f'a'\r\nif")
+h.equals("what a browser breaks at is escaped between tokens", out,
+ "<span class='hl-keyword'>if</span><span class='escape'>\\r</span>" ..
+ "<span class='hl-number'>42</span><span class='escape'>\\v</span>" ..
+ "<span class='hl-keyword'>zz</span><span class='escape'>\\f</span>" ..
+ "<span class='hl-string'>'a'</span>\n<span class='hl-keyword'>if</span>")
+
+-- The fake lexer hands back every byte of a separator as a token of its own.
+out = render("hello.fake", "a\194\133b\226\128\168c\226\128\169d")
+h.equals("a separator split between tokens is escaped whole", out,
+ "<span class='hl-keyword'>a</span><span class='escape'>\\u0085</span>" ..
+ "<span class='hl-keyword'>b</span><span class='escape'>\\u2028</span>" ..
+ "<span class='hl-keyword'>c</span><span class='escape'>\\u2029</span>" ..
+ "<span class='hl-keyword'>d</span>")
+
out = render("big.fake", string.rep("a", 512 * 1024 + 1))
h.excludes("an oversized blob skips the lexing pass", out, "<span")
h.equals("and still arrives whole", #out, 512 * 1024 + 1)
diff --git a/tests/t0005-markup.sh b/tests/t0005-markup.sh
index e3cb08d..5d802a7 100755
--- a/tests/t0005-markup.sh
+++ b/tests/t0005-markup.sh
@@ -90,21 +90,31 @@ test_expect_success 'the blob, blame and about pages open with their own contain
grep "<div id=.about.>" tmp
'
+# A browser gives these elements a monospace font of its own, which would
+# otherwise win over the one a theme sets through --font-mono.
+test_expect_success 'every monospace element takes the theme font' '
+ sed -n "/^div#cgit code,$/,/^}/p" "$TEST_OUTPUT_DIRECTORY/../assets/cgit.css" >rule &&
+ grep "^div#cgit pre,$" rule &&
+ grep "^ font-family: var(--font-mono);$" rule
+'
+
test_expect_success 'the stylesheet and the script name only the current hooks' '
! grep "_dark\|#summary\|ul.pager\|td:empty" "$TEST_OUTPUT_DIRECTORY/../assets/cgit.css" &&
grep "window.cgit.updateAges" "$TEST_OUTPUT_DIRECTORY/../assets/cgit.js" &&
grep "window.cgit.highlightLines" "$TEST_OUTPUT_DIRECTORY/../assets/cgit.js"
'
-# The rest of the hooks need a tag, a symlink, a submodule, a trailer, clone
-# urls, an empty repository and a second page of repositories, so they get a
-# commit and a config of their own after the checks above have read the first
-# ones.
+# The rest of the hooks need a tag, a symlink, a submodule, a trailer, a lone
+# carriage return, clone urls, an empty repository and a second page of
+# repositories, so they get a commit and a config of their own after the checks
+# above have read the first ones.
test_expect_success 'set up the remaining kinds of entry' '
(
cd repos/hooks &&
git tag -a -m "first release" v1 &&
test_ln_s_add file-1 link &&
+ printf "a\\rb\\n" >cr.txt &&
+ git add cr.txt &&
git update-index --add --cacheinfo "160000,$(git rev-parse HEAD),sub" &&
git commit -m "more kinds of entry" -m "Fixes: $(git rev-parse --short HEAD)"
) &&
@@ -139,6 +149,11 @@ test_expect_success 'the refs, summary and tree name every section and row' '
grep "<tr class=.mod.>" tmp
'
+test_expect_success 'a character the browser would break a line at is an escape' '
+ more "hooks/tree/cr.txt" >tmp &&
+ grep "a<span class=.escape.>" tmp
+'
+
test_expect_success 'the commit page has its trailer table and diffstat' '
more "hooks/commit/" >tmp &&
grep "<table class=.commit-trailers.>" tmp &&
diff --git a/tests/t0104-tree.sh b/tests/t0104-tree.sh
index b48c394..d464f8c 100755
--- a/tests/t0104-tree.sh
+++ b/tests/t0104-tree.sh
@@ -22,6 +22,66 @@ test_expect_success 'no line 2' '
! grep "<a id=.n2. href=.#n2.>2</a>" tmp
'
+# The numbers and the code are two columns of text that a browser keeps in
+# step only while both are set in one font. Only a browser shows the drift,
+# so this checks the rule that prevents it.
+test_expect_success 'the code is set in the font of its line numbers' '
+ sed -n "/^div#cgit table.blob code {/,/^}/p" \
+ "$TEST_OUTPUT_DIRECTORY/../assets/cgit.css" >rule &&
+ grep "^ font: inherit;$" rule
+'
+
+# A browser starts a new line at more characters than git ends one at, and
+# each would push the rest of the code a line below its numbers.
+test_expect_success 'set up a file holding the characters a browser breaks at' '
+ test_create_repo repos/breaks &&
+ (
+ cd repos/breaks &&
+ printf "a\\rb\\vc\\fd\\302\\205e\\342\\200\\250f\\342\\200\\251g\\nh\\r\\ni\\r" >breaks.txt &&
+ git add breaks.txt &&
+ git commit -m breaks
+ ) &&
+ {
+ echo "virtual-root=/" &&
+ echo "cache-size=0" &&
+ echo "enable-blame=1" &&
+ echo "repo.url=breaks" &&
+ echo "repo.path=$PWD/repos/breaks/.git"
+ } >breaksrc
+'
+
+test_expect_success 'each of them is shown as an escape in the tree' '
+ CGIT_CONFIG="$PWD/breaksrc" QUERY_STRING="url=breaks/tree/breaks.txt" cgit >tmp &&
+ grep "<code>a<span class=.escape.>\\\\r</span>b<span class=.escape.>\\\\v</span>c" tmp &&
+ grep "c<span class=.escape.>\\\\f</span>d<span class=.escape.>\\\\u0085</span>e" tmp &&
+ grep "e<span class=.escape.>\\\\u2028</span>f<span class=.escape.>\\\\u2029</span>g$" tmp
+'
+
+test_expect_success 'only a carriage return before a line feed is left as it is' '
+ printf "h\\r\\n" >expect &&
+ grep "^h" tmp >actual &&
+ test_cmp expect actual &&
+ grep "^i<span class=.escape.>\\\\r</span></code>" tmp
+'
+
+test_expect_success 'the numbers count only the lines git counts' '
+ grep "<a id=.n3. href=.#n3.>3</a>" tmp &&
+ ! grep "<a id=.n4. href=.#n4.>4</a>" tmp
+'
+
+test_expect_success 'the blame page escapes them the same way' '
+ CGIT_CONFIG="$PWD/breaksrc" QUERY_STRING="url=breaks/blame/breaks.txt" cgit >tmp &&
+ grep "<code>a<span class=.escape.>\\\\r</span>b<span class=.escape.>\\\\v</span>c" tmp &&
+ grep "e<span class=.escape.>\\\\u2028</span>f<span class=.escape.>\\\\u2029</span>g$" tmp &&
+ grep "^i<span class=.escape.>\\\\r</span></code>" tmp
+'
+
+# The striped background is padded with spaces to the widest line, which is
+# 31 columns once its six escapes are written out, though it is 18 bytes.
+test_expect_success 'the blame stripes are as wide as the escaped line' '
+ grep "^ \\{31\\}</pre></div>" tmp
+'
+
test_expect_success 'generate foo+bar/tree' 'cgit_url "foo%2bbar/tree" >tmp'
test_expect_success 'verify a+b link' '
diff --git a/tests/t0109-patch.sh b/tests/t0109-patch.sh
index 3cb9d08..0c4bf8c 100755
--- a/tests/t0109-patch.sh
+++ b/tests/t0109-patch.sh
@@ -8,9 +8,13 @@
test_description='Check content on patch page'
. ./setup.sh
+# The version the built binary signs its patches with, without the v of the
+# tag, as git writes its own.
+CGIT_VERSION=$(sed -n "s/CGIT_VERSION = v\{0,1\}//p" "$TEST_OUTPUT_DIRECTORY/../build/VERSION")
+
# The signature sits on the second-to-last line, above the trailing blank.
check_cgit_signature() {
- tail -2 tmp | head -1 | grep "^cgit$"
+ tail -2 tmp | head -1 | grep -x -F "cgit $CGIT_VERSION"
}
test_expect_success 'generate foo/patch' '
@@ -35,7 +39,7 @@ test_expect_success 'find `cgit` signature' '
test_expect_success 'compare with output of git-format-patch(1)' '
git --git-dir="$PWD/repos/foo/.git" format-patch --subject-prefix="" \
- --signature="cgit" --stdout HEAD^ >tmp2 &&
+ --signature="cgit $CGIT_VERSION" --stdout HEAD^ >tmp2 &&
strip_headers <tmp >tmp_ &&
test_cmp tmp_ tmp2
'
@@ -64,7 +68,7 @@ test_expect_success 'find `cgit` signature on the range' '
test_expect_success 'compare the range with git-format-patch(1)' '
git --git-dir="$PWD/repos/foo/.git" format-patch --subject-prefix="" \
- -N --signature="cgit" --stdout HEAD~3..HEAD >tmp2 &&
+ -N --signature="cgit $CGIT_VERSION" --stdout HEAD~3..HEAD >tmp2 &&
strip_headers <tmp >tmp_ &&
test_cmp tmp_ tmp2
'
diff --git a/tests/t0502-syntax-highlight.sh b/tests/t0502-syntax-highlight.sh
index 31ab7e7..ced9edd 100755
--- a/tests/t0502-syntax-highlight.sh
+++ b/tests/t0502-syntax-highlight.sh
@@ -70,4 +70,15 @@ test_expect_success CGIT_LUA 'bytes between tokens reach the page escaped' '
grep "<span class=.hl-number.>42</span> &amp; " tmp
'
+test_expect_success CGIT_LUA 'a lone carriage return reaches the page as an escape' '
+ (
+ cd repos/fake &&
+ printf "if\\rzz\\n" >cr.fake &&
+ git add cr.fake &&
+ git commit -m "add a lone carriage return"
+ ) &&
+ cgit_url "fake/tree/cr.fake" >tmp &&
+ grep "<span class=.hl-keyword.>if</span><span class=.escape.>\\\\r</span>" tmp
+'
+
test_done