diff options
context:
space:
mode:
Diffstat (limited to 'source')
-rw-r--r--source/ui-atom.c87
-rw-r--r--source/ui-blame.c29
-rw-r--r--source/ui-blob.c42
-rw-r--r--source/ui-blob.h6
-rw-r--r--source/ui-clone.c9
-rw-r--r--source/ui-commit.c49
-rw-r--r--source/ui-diff.c88
-rw-r--r--source/ui-empty.c4
-rw-r--r--source/ui-log.c97
-rw-r--r--source/ui-patch.c35
-rw-r--r--source/ui-plain.c11
-rw-r--r--source/ui-refs.c47
-rw-r--r--source/ui-repolist.c11
-rw-r--r--source/ui-shared.c187
-rw-r--r--source/ui-shared.h24
-rw-r--r--source/ui-snapshot.c71
-rw-r--r--source/ui-snapshot.h6
-rw-r--r--source/ui-ssdiff.c46
-rw-r--r--source/ui-stats.c56
-rw-r--r--source/ui-stats.h6
-rw-r--r--source/ui-summary.c17
-rw-r--r--source/ui-summary.h4
-rw-r--r--source/ui-tag.c15
-rw-r--r--source/ui-tree.c61
24 files changed, 658 insertions, 350 deletions
diff --git a/source/ui-atom.c b/source/ui-atom.c
index 46039a0..b3265b7 100644
--- a/source/ui-atom.c
+++ b/source/ui-atom.c
@@ -14,6 +14,9 @@
#include "ui-atom.h"
#include "ui-shared.h"
+// Stands in for every byte the feed cannot carry.
+#define XML_REPLACEMENT "?"
+
/*
* Atom timestamps have to be RFC 3339, so a feed ignores the date-format and
* local-time settings the browsable pages honour. The zero is the timezone
@@ -24,9 +27,6 @@ static const char *feed_date(timestamp_t when)
return show_date(when, 0, date_mode_from_type(DATE_ISO8601_STRICT));
}
-// Stands in for every byte the feed cannot carry.
-#define XML_REPLACEMENT "?"
-
/*
* How many bytes the UTF-8 sequence at p holds, or 0 when invalid. The
* lead-byte ranges fold in the overlong, surrogate and out-of-range cases,
@@ -130,7 +130,8 @@ static void print_email(const char *email)
static void print_entry(struct commit *commit, const char *host)
{
struct commitinfo *info;
- char *hex;
+ char *hex, *pageurl;
+ char delim = '&';
info = cgit_parse_commit(commit);
hex = oid_to_hex(&commit->object.oid);
@@ -158,24 +159,18 @@ static void print_entry(struct commit *commit, const char *host)
html("<published>");
xml_txt(feed_date(info->author_date));
html("</published>\n");
- {
- char *pageurl;
- char delim = '&';
-
- html("<link rel='alternate' type='text/html' href='");
- html(cgit_httpscheme());
- html_attr(host);
- pageurl = cgit_pageurl(ctx.repo->url, "commit", NULL);
- html_attr(pageurl);
- // Without a virtual root the page url is already a query
- // string, so the commit id continues it instead of opening
- // one.
- if (ctx.cfg.virtual_root)
- delim = '?';
- html_attrf("%cid=%s", delim, hex);
- html("'/>\n");
- free(pageurl);
- }
+ html("<link rel='alternate' type='text/html' href='");
+ html(cgit_httpscheme());
+ html_attr(host);
+ pageurl = cgit_pageurl(ctx.repo->url, "commit", NULL);
+ html_attr(pageurl);
+ // Without a virtual root the page url is already a query string, so
+ // the commit id continues it instead of opening one.
+ if (ctx.cfg.virtual_root)
+ delim = '?';
+ html_attrf("%cid=%s", delim, hex);
+ html("'/>\n");
+ free(pageurl);
html("<id>");
html_txtf("urn:%s:%s", the_hash_algo->name, hex);
html("</id>\n");
@@ -188,12 +183,13 @@ static void print_entry(struct commit *commit, const char *host)
void cgit_print_atom(char *tip, const char *path, int max_count)
{
- char *host;
+ char *host, *fullurl, *repourl;
// setup_revisions reads a command line, so the first slot is the
// unused program name and parsing starts at the second.
- const char *argv[] = {NULL, tip, NULL, NULL, NULL};
+ const char *argv[] = { NULL, tip, NULL, NULL, NULL };
struct commit *commit;
struct rev_info rev;
+ struct strbuf idbuf = STRBUF_INIT;
int argc = 2;
bool need_updated = true;
@@ -214,7 +210,12 @@ void cgit_print_atom(char *tip, const char *path, int max_count)
rev.show_root_diff = 0;
rev.max_count = max_count;
setup_revisions(argc, argv, &rev, NULL);
- prepare_revision_walk(&rev);
+ // A failed setup leaves the walk holding freed commits, so it must
+ // not be read from.
+ if (prepare_revision_walk(&rev)) {
+ cgit_print_error_page(500, "Internal Server Error", "Unable to read the history");
+ return;
+ }
// CGI guarantees a server name, so only a bare test run reaches the
// fallback, which keeps the mandatory feed id and links present.
@@ -240,26 +241,22 @@ void cgit_print_atom(char *tip, const char *path, int max_count)
html("<subtitle>");
xml_txt(ctx.repo->desc);
html("</subtitle>\n");
- {
- char *fullurl = cgit_currentfullurl();
- char *repourl = cgit_repourl(ctx.repo->url);
- struct strbuf idbuf = STRBUF_INIT;
-
- strbuf_addf(&idbuf, "%s%s%s", cgit_httpscheme(), host, fullurl);
- html("<id>");
- xml_txt(idbuf.buf);
- html("</id>\n");
- strbuf_release(&idbuf);
- html("<link rel='self' href='");
- html_attrf("%s%s%s", cgit_httpscheme(), host, fullurl);
- html("'/>\n");
- html("<link rel='alternate' type='text/html' href='");
- html_attrf("%s%s%s", cgit_httpscheme(), host, repourl);
- html("'/>\n");
- free(fullurl);
- free(repourl);
- }
- while ((commit = get_revision(&rev)) != NULL) {
+ fullurl = cgit_currentfullurl();
+ repourl = cgit_repourl(ctx.repo->url);
+ strbuf_addf(&idbuf, "%s%s%s", cgit_httpscheme(), host, fullurl);
+ html("<id>");
+ xml_txt(idbuf.buf);
+ html("</id>\n");
+ strbuf_release(&idbuf);
+ html("<link rel='self' href='");
+ html_attrf("%s%s%s", cgit_httpscheme(), host, fullurl);
+ html("'/>\n");
+ html("<link rel='alternate' type='text/html' href='");
+ html_attrf("%s%s%s", cgit_httpscheme(), host, repourl);
+ html("'/>\n");
+ free(fullurl);
+ free(repourl);
+ while ((commit = get_revision(&rev))) {
if (need_updated) {
html("<updated>");
xml_txt(feed_date(commit->date));
diff --git a/source/ui-blame.c b/source/ui-blame.c
index 7248120..c020dbc 100644
--- a/source/ui-blame.c
+++ b/source/ui-blame.c
@@ -17,10 +17,6 @@
#include "ui-blame.h"
#include "ui-shared.h"
-// A tab in the rendered source runs on to the next multiple of this,
-// matching what the browser does on its own since tab-size is left alone.
-#define TAB_WIDTH 8
-
enum blame_target {
TARGET_MISSING,
TARGET_FILE,
@@ -116,14 +112,15 @@ static char *suspect_detail(struct blame_origin *suspect)
info = cgit_parse_commit(suspect->commit);
- strbuf_addf(&detail, "author %s", info->author);
- if (ctx.cfg.enable_plain_email)
+ // A commit object may lack either ident line, leaving the fields NULL.
+ strbuf_addf(&detail, "author %s", info->author ? info->author : "");
+ if (ctx.cfg.enable_plain_email && info->author_email)
strbuf_addf(&detail, " %s", info->author_email);
strbuf_addf(&detail, " %s\n",
show_date(info->author_date, info->author_tz, cgit_date_mode(DATE_ISO8601)));
- strbuf_addf(&detail, "committer %s", info->committer);
- if (ctx.cfg.enable_plain_email)
+ strbuf_addf(&detail, "committer %s", info->committer ? info->committer : "");
+ if (ctx.cfg.enable_plain_email && info->committer_email)
strbuf_addf(&detail, " %s", info->committer_email);
strbuf_addf(&detail, " %s\n\n",
show_date(info->committer_date, info->committer_tz, cgit_date_mode(DATE_ISO8601)));
@@ -179,12 +176,12 @@ static void emit_hashes(struct blame_scoreboard *sb)
static void emit_entry_linenumbers(struct blame_entry *ent)
{
- const char *numberfmt = "<a id='n%1$d' href='#n%1$d'>%1$d</a>\n";
struct strbuf numbers = STRBUF_INIT;
int lineno = ent->lno;
while (lineno < ent->lno + ent->num_lines) {
- strbuf_addf(&numbers, numberfmt, ++lineno);
+ lineno++;
+ strbuf_addf(&numbers, "<a id='n%d' href='#n%d'>%d</a>\n", lineno, lineno, lineno);
if (numbers.len >= HTML_BATCH) {
html_raw(numbers.buf, numbers.len);
strbuf_reset(&numbers);
@@ -274,7 +271,7 @@ static void print_blame_page(const struct object_id *oid, const char *path,
type = odb_read_object_info(the_repository->objects, oid, &size);
if (type == OBJ_BAD) {
- cgit_print_error_page(404, "Not Found", "Bad object name: %s", oid_to_hex(oid));
+ cgit_print_error_page(404, "Not Found", "Bad object id: %s", oid_to_hex(oid));
return;
}
if (ctx.cfg.max_blob_size && size / 1024 > (unsigned long)ctx.cfg.max_blob_size) {
@@ -287,7 +284,8 @@ static void print_blame_page(const struct object_id *oid, const char *path,
buf = odb_read_object(the_repository->objects, oid, &type, &size);
if (!buf) {
- cgit_print_error_page(500, "Internal Server Error", "Error reading object %s", oid_to_hex(oid));
+ cgit_print_error_page(500, "Internal Server Error", "Unable to read object %s",
+ oid_to_hex(oid));
return;
}
@@ -333,6 +331,7 @@ static void print_blame_page(const struct object_id *oid, const char *path,
html("<pre><code>");
if (ctx.repo->source_filter) {
char *filter_arg = xstrdup(filename);
+
cgit_open_filter(ctx.repo->source_filter, filter_arg);
html_raw(buf, size);
cgit_close_filter(ctx.repo->source_filter);
@@ -412,12 +411,12 @@ void cgit_print_blame(void)
rev = ctx.qry.head;
if (repo_get_oid(the_repository, rev, &oid)) {
- cgit_print_error_page(404, "Not Found", "Invalid revision name: %s", rev);
+ cgit_print_error_page(404, "Not Found", "Bad object id: %s", rev);
return;
}
commit = lookup_commit_reference(the_repository, &oid);
if (!commit || repo_parse_commit(the_repository, commit)) {
- cgit_print_error_page(404, "Not Found", "Invalid commit reference: %s", rev);
+ cgit_print_error_page(404, "Not Found", "Not a commit: %s", rev);
return;
}
@@ -428,7 +427,7 @@ void cgit_print_blame(void)
if (walk.found == TARGET_MISSING)
cgit_print_error_page(404, "Not Found", "Not found");
else if (walk.found == TARGET_FOLDER)
- cgit_print_error_page(404, "Not Found", "Blame is not available for folders.");
+ cgit_print_error_page(404, "Not Found", "Blame is not available for a directory");
free(walk.rev);
}
diff --git a/source/ui-blob.c b/source/ui-blob.c
index a298e54..8407342 100644
--- a/source/ui-blob.c
+++ b/source/ui-blob.c
@@ -22,6 +22,18 @@ struct walk_tree_context {
};
/*
+ * An annotated tag names a commit through its tag object, and a path is looked
+ * up in the commit's tree, not in the tag.
+ */
+static void peel_to_commit(struct object_id *oid)
+{
+ struct commit *commit = lookup_commit_reference_gently(the_repository, oid, 1);
+
+ if (commit)
+ oidcpy(oid, &commit->object.oid);
+}
+
+/*
* read_tree reads the return value as a direction rather than a status, so
* READ_TREE_RECURSIVE means step into this entry and zero means step over it.
*/
@@ -30,9 +42,14 @@ static int walk_tree(const struct object_id *oid, struct strbuf *base,
{
struct walk_tree_context *walk = context;
+ // Stepping into a submodule entry would have git look its commit up
+ // in this repository, which does not hold it, and die.
if (walk->file_only && !S_ISREG(mode))
- return READ_TREE_RECURSIVE;
- if (strncmp(base->buf, walk->match_path, base->len) || strcmp(walk->match_path + base->len, pathname))
+ return S_ISDIR(mode) ? READ_TREE_RECURSIVE : 0;
+ if (
+ strncmp(base->buf, walk->match_path, base->len) ||
+ strcmp(walk->match_path + base->len, pathname)
+ )
return READ_TREE_RECURSIVE;
oidcpy(walk->matched_oid, oid);
walk->found_path = 1;
@@ -47,6 +64,7 @@ static int walk_tree(const struct object_id *oid, struct strbuf *base,
static int find_path_oid(struct object_id *oid, char *path, int file_only)
{
struct commit *commit = lookup_commit_reference(the_repository, oid);
+ struct tree *tree;
// nowildcard_len matching len makes git treat the path as literal
// rather than as a glob.
struct pathspec_item item = {
@@ -65,7 +83,14 @@ static int find_path_oid(struct object_id *oid, char *path, int file_only)
.file_only = file_only
};
- read_tree(the_repository, repo_get_commit_tree(the_repository, commit), &paths, walk_tree, &walk);
+ // A commit git cannot parse, one with a broken tree line for example,
+ // comes back null and has no tree to search.
+ if (!commit)
+ return 0;
+ tree = repo_get_commit_tree(the_repository, commit);
+ if (!tree)
+ return 0;
+ read_tree(the_repository, tree, &paths, walk_tree, &walk);
return walk.found_path;
}
@@ -105,6 +130,7 @@ int cgit_print_file(char *path, const char *head, int file_only, int html_escape
if (repo_get_oid(the_repository, head, &oid))
return -1;
+ peel_to_commit(&oid);
type = odb_read_object_info(the_repository->objects, &oid, &size);
if (type == OBJ_COMMIT) {
if (!find_path_oid(&oid, path, file_only))
@@ -152,14 +178,15 @@ void cgit_print_blob(const char *hex, char *path, const char *head, int file_onl
if (hex) {
if (get_oid_hex(hex, &oid)) {
- cgit_print_error_page(400, "Bad Request", "Bad hex value: %s", hex);
+ cgit_print_error_page(404, "Not Found", "Bad object id: %s", hex);
return;
}
} else {
if (repo_get_oid(the_repository, head, &oid)) {
- cgit_print_error_page(404, "Not Found", "Bad ref: %s", head);
+ cgit_print_error_page(404, "Not Found", "Bad object id: %s", head);
return;
}
+ peel_to_commit(&oid);
}
type = odb_read_object_info(the_repository->objects, &oid, &size);
@@ -173,7 +200,7 @@ void cgit_print_blob(const char *hex, char *path, const char *head, int file_onl
}
if (type == OBJ_BAD) {
- cgit_print_error_page(404, "Not Found", "Bad object name: %s", hex ? hex : path);
+ cgit_print_error_page(404, "Not Found", "Bad object id: %s", hex ? hex : path);
return;
}
@@ -185,7 +212,8 @@ void cgit_print_blob(const char *hex, char *path, const char *head, int file_onl
buf = odb_read_object(the_repository->objects, &oid, &type, &size);
if (!buf) {
- cgit_print_error_page(500, "Internal Server Error", "Error reading object %s", hex ? hex : path);
+ cgit_print_error_page(500, "Internal Server Error", "Unable to read object %s",
+ hex ? hex : path);
return;
}
diff --git a/source/ui-blob.h b/source/ui-blob.h
index 2eab0db..58e0abb 100644
--- a/source/ui-blob.h
+++ b/source/ui-blob.h
@@ -1,7 +1,7 @@
/*
- * Declarations for reading a blob out of the object database and writing it to
- * the client. A caller either hands the whole response over to the blob's own
- * bytes and headers, or drops a file's contents into a page cgit is already
+ * The blob page, which reads a blob out of the object database and writes it
+ * to the client. A caller either hands the whole response over to the blob's
+ * own bytes and headers, or drops a file's contents into a page cgit is already
* building, as the summary page does for a readme.
*/
diff --git a/source/ui-clone.c b/source/ui-clone.c
index 4329e9d..2544e9f 100644
--- a/source/ui-clone.c
+++ b/source/ui-clone.c
@@ -72,6 +72,7 @@ static void print_pack_info(void)
// directly leaves those packs unfindable.
for (entry = packfile_store_get_packs(files->packed); entry; entry = entry->next) {
struct packed_git *pack = entry->pack;
+
if (pack->pack_local)
htmlf("P %s\n", last_path_component(pack->pack_name));
}
@@ -79,8 +80,7 @@ static void print_pack_info(void)
}
/*
- * Beyond the obvious directory traversal, the strict character set heads off
- * other funny business, for example the file name quirks of the Cygwin port.
+ * Only the characters an object path can hold pass, and no dotdot component.
*/
static int path_is_safe(const char *path)
{
@@ -112,6 +112,11 @@ static void send_file(const char *path)
}
return;
}
+ // fopen opens a directory on most systems and reads nothing from it.
+ if (!S_ISREG(st.st_mode)) {
+ cgit_print_error_page(404, "Not Found", "Not found");
+ return;
+ }
ctx.page.mimetype = "application/octet-stream";
// Offer the file under its path inside the repository, so the layout
// of the server's disk stays out of the download name.
diff --git a/source/ui-commit.c b/source/ui-commit.c
index 80c7faf..1fdc5fa 100644
--- a/source/ui-commit.c
+++ b/source/ui-commit.c
@@ -32,7 +32,7 @@ static void print_ident_row(const char *role, const char *name, const char *emai
timestamp_t date, int tz)
{
htmlf("<tr><th>%s</th><td>", role);
- cgit_open_filter(ctx.repo->email_filter, email, "commit");
+ cgit_open_filter(ctx.repo->email_filter, email ? email : "", "commit");
html_txt(name);
if (ctx.cfg.enable_plain_email) {
html(" ");
@@ -46,10 +46,16 @@ static void print_ident_row(const char *role, const char *name, const char *emai
html("</time></td></tr>\n");
}
-static int print_parent_rows(struct commit *commit, const char *rev, const char *prefix)
+/*
+ * Counts the parents that could be read and leaves the first of them in first,
+ * which the diff below the message is taken against.
+ */
+static int print_parent_rows(struct commit *commit, const char *rev, const char *prefix,
+ struct object_id *first)
{
struct commit_list *p;
struct commit *parent;
+ struct commitinfo *info;
const char *parent_hex, *label;
int parents = 0;
@@ -61,13 +67,19 @@ static int print_parent_rows(struct commit *commit, const char *rev, const char
html("</td></tr>\n");
continue;
}
+ if (!parents)
+ oidcpy(first, &p->item->object.oid);
html("<tr><th>parent</th><td colspan='2' class='oid'><div class='spanning'>");
parent_hex = label = oid_to_hex(&p->item->object.oid);
- if (ctx.repo->enable_subject_links)
- label = cgit_parse_commit(parent)->subject;
+ info = ctx.repo->enable_subject_links ? cgit_parse_commit(parent) : NULL;
+ if (info)
+ label = info->subject;
cgit_commit_link(label, NULL, NULL, ctx.qry.head, parent_hex, prefix);
+ if (info)
+ cgit_free_commitinfo(info);
html(" (");
- cgit_diff_link("diff", NULL, NULL, ctx.qry.head, rev, oid_to_hex(&p->item->object.oid), prefix);
+ cgit_diff_link("diff", NULL, NULL, ctx.qry.head, rev, oid_to_hex(&p->item->object.oid),
+ prefix);
html(")</div></td></tr>\n");
parents++;
}
@@ -127,8 +139,9 @@ static void print_trailer_value(const char *key, const char *value)
cgit_open_filter(ctx.repo->trailer_filter, key, "commit");
html_txt(value);
cgit_close_filter(ctx.repo->trailer_filter);
- } else
+ } else {
print_filtered_text(value);
+ }
}
/*
@@ -204,7 +217,10 @@ static void print_message(struct commitinfo *info)
end = trailer_block_end(block);
free_trailers(&items);
trailer_block_release(block);
- if (start >= prefix && start < end) {
+ // git leaves trailing comment lines and an old style Conflicts
+ // section after the block, which the split would lose, so such
+ // a message is shown whole.
+ if (start >= prefix && start < end && !full.buf[end + strspn(full.buf + end, "\n")]) {
size_t len = start - prefix;
while (len > 0 && info->msg[len - 1] == '\n')
@@ -228,8 +244,9 @@ void cgit_print_commit(char *hex, const char *prefix)
struct commit *commit;
struct commitinfo *info;
struct strbuf notes = STRBUF_INIT;
- struct object_id oid;
- const char *commit_hex, *first_parent;
+ struct object_id oid, first_oid;
+ const char *commit_hex;
+ char *first_parent;
char *tree_rev;
int parents;
@@ -237,17 +254,17 @@ void cgit_print_commit(char *hex, const char *prefix)
hex = ctx.qry.head;
if (repo_get_oid(the_repository, hex, &oid)) {
- cgit_print_error_page(400, "Bad Request", "Bad object id: %s", hex);
+ cgit_print_error_page(404, "Not Found", "Bad object id: %s", hex);
return;
}
commit = lookup_commit_reference(the_repository, &oid);
if (!commit) {
- cgit_print_error_page(404, "Not Found", "Bad commit reference: %s", hex);
+ cgit_print_error_page(404, "Not Found", "Not a commit: %s", hex);
return;
}
info = cgit_parse_commit(commit);
- format_display_notes(&oid, &notes, PAGE_ENCODING, 1);
+ format_display_notes(&commit->object.oid, &notes, PAGE_ENCODING, 1);
load_ref_decorations(NULL, DECORATE_FULL_REFS);
@@ -271,7 +288,7 @@ void cgit_print_commit(char *hex, const char *prefix)
// The parent rows follow the commit row so the commit hashes sit
// together, leaving the tree beside the download links.
- parents = print_parent_rows(commit, hex, prefix);
+ parents = print_parent_rows(commit, hex, prefix, &first_oid);
html("<tr><th>tree</th><td colspan='2' class='oid'><div class='spanning'>");
tree_rev = xstrdup(hex);
@@ -304,11 +321,9 @@ void cgit_print_commit(char *hex, const char *prefix)
}
if (parents < OCTOPUS_PARENTS) {
- if (parents)
- first_parent = oid_to_hex(&commit->parents->item->object.oid);
- else
- first_parent = NULL;
+ first_parent = parents ? xstrdup(oid_to_hex(&first_oid)) : NULL;
cgit_print_diff(ctx.qry.oid, first_parent, prefix, 0, 0);
+ free(first_parent);
}
strbuf_release(&notes);
diff --git a/source/ui-diff.c b/source/ui-diff.c
index fb86a96..ee8dc76 100644
--- a/source/ui-diff.c
+++ b/source/ui-diff.c
@@ -23,10 +23,6 @@
// and a second walk renders the page instead.
#define BODY_BUDGET (8 * 1024 * 1024)
-// What a context of zero means once the diff runs, mirroring the fallback in
-// cgit_diff_files, so the control offers the value the diff will really use.
-#define DEFAULT_CONTEXT_LINES 3
-
struct fileinfo {
char status;
struct object_id old_oid[1];
@@ -120,7 +116,7 @@ static void print_fileinfo(struct fileinfo *info)
class = "stg";
break;
default:
- die("bug: unhandled diff status %c", info->status);
+ BUG("unhandled diff status %c", info->status);
}
html("<tr>");
@@ -131,13 +127,18 @@ static void print_fileinfo(struct fileinfo *info)
cgit_print_filemode(info->new_mode);
}
- if (info->old_mode != info->new_mode && !is_null_oid(info->old_oid) && !is_null_oid(info->new_oid)) {
+ if (
+ info->old_mode != info->new_mode &&
+ !is_null_oid(info->old_oid) &&
+ !is_null_oid(info->new_oid)
+ ) {
html("<span class='modechange'>[");
cgit_print_filemode(info->old_mode);
html("]</span>");
}
htmlf("</td><td class='%s'>", class);
- cgit_diff_link(info->new_path, NULL, NULL, ctx.qry.head, ctx.qry.oid, ctx.qry.oid2, info->new_path);
+ cgit_diff_link(info->new_path, NULL, NULL, ctx.qry.head, ctx.qry.oid, ctx.qry.oid2,
+ info->new_path);
if (info->status == DIFF_STATUS_COPIED || info->status == DIFF_STATUS_RENAMED) {
htmlf(" (%s from ", info->status == DIFF_STATUS_COPIED ? "copied" : "renamed");
html_txt(info->old_path);
@@ -145,10 +146,11 @@ static void print_fileinfo(struct fileinfo *info)
}
html("</td><td class='right'>");
if (info->binary) {
- htmlf("bin</td><td class='graph'>%lu -> %lu bytes</td></tr>\n", info->old_size, info->new_size);
+ htmlf("bin</td><td class='graph'>%lu -> %lu bytes</td></tr>\n", info->old_size,
+ info->new_size);
return;
}
- htmlf("%d", info->added + info->removed);
+ htmlf("%u", info->added + info->removed);
html("</td><td class='graph'>");
html("<table><tr>");
add_span = (int)(info->added * 1000.0 / scale + 0.5);
@@ -173,7 +175,11 @@ static void count_diff_lines(char *line, int len)
}
if (!render_line_fn || render_suppressed)
return;
- if (cap_diffs && ctx.cfg.max_diff_lines > 0 && lines_added + lines_removed > ctx.cfg.max_diff_lines) {
+ if (
+ cap_diffs &&
+ ctx.cfg.max_diff_lines > 0 &&
+ lines_added + lines_removed > ctx.cfg.max_diff_lines
+ ) {
render_suppressed = 1;
return;
}
@@ -184,7 +190,8 @@ static int show_filepair(struct diff_filepair *pair)
{
if (!current_prefix)
return 1;
- return starts_with(pair->one->path, current_prefix) || starts_with(pair->two->path, current_prefix);
+ return starts_with(pair->one->path, current_prefix) ||
+ starts_with(pair->two->path, current_prefix);
}
/*
@@ -220,8 +227,8 @@ static char *abbrev_oid(const struct object_id *oid)
return xstrdup(repo_find_unique_abbrev(the_repository, oid, DEFAULT_ABBREV));
}
-static void print_file_header(const struct object_id *old_oid, char *old_path, int old_mode,
- const struct object_id *new_oid, char *new_path, int new_mode)
+static void print_file_header(const struct object_id *old_oid, char *old_path, unsigned old_mode,
+ const struct object_id *new_oid, char *new_path, unsigned new_mode)
{
char *old_abbrev, *new_abbrev;
int subproject;
@@ -253,8 +260,9 @@ static void print_file_header(const struct object_id *old_oid, char *old_path, i
if (is_null_oid(old_oid)) {
old_path = "dev/null";
html("<br>--- /");
- } else
+ } else {
html("<br>--- a/");
+ }
if (old_mode != 0)
cgit_tree_link(old_path, NULL, NULL, ctx.qry.head, oid_to_hex(old_rev_oid), old_path);
else
@@ -262,8 +270,9 @@ static void print_file_header(const struct object_id *old_oid, char *old_path, i
if (is_null_oid(new_oid)) {
new_path = "dev/null";
html("<br>+++ /");
- } else
+ } else {
html("<br>+++ b/");
+ }
if (new_mode != 0)
cgit_tree_link(new_path, NULL, NULL, ctx.qry.head, oid_to_hex(new_rev_oid), new_path);
else
@@ -303,13 +312,15 @@ static void print_truncated(const char *path)
else
html("<div class='truncated'>");
html("This diff is too large to be rendered inline. ");
- cgit_diff_link("View it on its own page", NULL, NULL, ctx.qry.head, ctx.qry.oid, ctx.qry.oid2, path);
+ cgit_diff_link("View it on its own page", NULL, NULL, ctx.qry.head, ctx.qry.oid, ctx.qry.oid2,
+ path);
html(".");
if (use_ssdiff) {
html("</td></tr>");
cgit_ssdiff_footer();
- } else
+ } else {
html("</div>");
+ }
}
static struct fileinfo *reserve_item(void)
@@ -552,7 +563,7 @@ void cgit_print_diff_ctrls(void)
html("<select name='context'>");
selected = ctx.qry.context;
if (!selected)
- selected = DEFAULT_CONTEXT_LINES;
+ selected = DEFAULT_DIFF_CONTEXT;
for (i = 1; i <= 10; i++)
html_intoption(i, cgit_fmt("%d", i), selected);
for (i = 15; i <= MAX_DIFF_CONTEXT_LINES; i += 5)
@@ -583,17 +594,37 @@ void cgit_print_diff_ctrls(void)
html("</div>\n");
}
+/*
+ * Whether the path names a directory in either tree. The diff below it can
+ * then still span many files, so the caps stay on.
+ */
+static int prefix_is_dir(const struct object_id *old_tree, const struct object_id *new_tree,
+ const char *prefix)
+{
+ struct object_id oid;
+ unsigned short mode;
+ char *path = xstrdup(prefix);
+ size_t len = strlen(path);
+ int is_dir = 0;
+
+ if (len && path[len - 1] == '/')
+ path[len - 1] = '\0';
+ if (new_tree && !get_tree_entry(the_repository, new_tree, path, &oid, &mode))
+ is_dir = S_ISDIR(mode);
+ else if (old_tree && !get_tree_entry(the_repository, old_tree, path, &oid, &mode))
+ is_dir = S_ISDIR(mode);
+ free(path);
+ return is_dir;
+}
+
void cgit_print_diff(const char *new_rev, const char *old_rev, const char *prefix,
int show_ctrls, int raw)
{
struct commit *new_commit, *old_commit;
const struct object_id *old_tree_oid, *new_tree_oid;
+ const char *path = prefix;
diff_type difftype;
- // Decided from the caller's prefix before the follow logic below
- // rewrites it to "", otherwise follow=1 silently disables the caps.
- cap_diffs = !prefix;
-
// Detecting renames needs the diff machinery to examine the whole
// commit, so with follow set the prefix is applied in show_filepair
// instead of being passed down.
@@ -607,19 +638,19 @@ void cgit_print_diff(const char *new_rev, const char *old_rev, const char *prefi
if (!new_rev)
new_rev = ctx.qry.head;
if (repo_get_oid(the_repository, new_rev, new_rev_oid)) {
- cgit_print_error_page(404, "Not Found", "Bad object name: %s", new_rev);
+ cgit_print_error_page(404, "Not Found", "Bad object id: %s", new_rev);
return;
}
new_commit = lookup_commit_reference(the_repository, new_rev_oid);
if (!new_commit || repo_parse_commit(the_repository, new_commit)) {
- cgit_print_error_page(404, "Not Found", "Bad commit: %s", oid_to_hex(new_rev_oid));
+ cgit_print_error_page(404, "Not Found", "Not a commit: %s", oid_to_hex(new_rev_oid));
return;
}
new_tree_oid = get_commit_tree_oid(new_commit);
if (old_rev) {
if (repo_get_oid(the_repository, old_rev, old_rev_oid)) {
- cgit_print_error_page(404, "Not Found", "Bad object name: %s", old_rev);
+ cgit_print_error_page(404, "Not Found", "Bad object id: %s", old_rev);
return;
}
} else if (new_commit->parents && new_commit->parents->item) {
@@ -631,7 +662,7 @@ void cgit_print_diff(const char *new_rev, const char *old_rev, const char *prefi
if (!is_null_oid(old_rev_oid)) {
old_commit = lookup_commit_reference(the_repository, old_rev_oid);
if (!old_commit || repo_parse_commit(the_repository, old_commit)) {
- cgit_print_error_page(404, "Not Found", "Bad commit: %s", oid_to_hex(old_rev_oid));
+ cgit_print_error_page(404, "Not Found", "Not a commit: %s", oid_to_hex(old_rev_oid));
return;
}
old_tree_oid = get_commit_tree_oid(old_commit);
@@ -639,6 +670,11 @@ void cgit_print_diff(const char *new_rev, const char *old_rev, const char *prefi
old_tree_oid = NULL;
}
+ // A path narrows the diff to one file, whose own page always renders
+ // in full. Decided from the caller's path, since the follow logic
+ // above has emptied the prefix.
+ cap_diffs = !path || prefix_is_dir(old_tree_oid, new_tree_oid, path);
+
if (raw) {
print_raw_patch(old_tree_oid, new_tree_oid);
return;
diff --git a/source/ui-empty.c b/source/ui-empty.c
index dd8dc64..30887c5 100644
--- a/source/ui-empty.c
+++ b/source/ui-empty.c
@@ -24,8 +24,8 @@ void cgit_print_empty_repo(void)
{
cgit_print_error("Repository seems to be empty");
- // cgit_add_clone_urls draws on just these two, so with neither set the
- // table below would be a Clone heading with no rows.
+ // cgit_add_clone_urls draws on these two alone, so with neither set
+ // the table below would be a Clone heading with no rows.
if (!ctx.repo->clone_url && !ctx.cfg.clone_prefix)
return;
diff --git a/source/ui-log.c b/source/ui-log.c
index 316df75..86cb756 100644
--- a/source/ui-log.c
+++ b/source/ui-log.c
@@ -176,6 +176,9 @@ static void wrap_subject(struct commitinfo *info, struct strbuf *msg)
--cut;
if (!cut)
cut = ctx.cfg.max_msg_len - strlen(wrap_symbol);
+ // A cut inside a multibyte character would leave both halves invalid.
+ while (cut > 0 && (info->subject[cut] & 0xC0) == 0x80)
+ --cut;
strbuf_add(msg, info->subject + cut, subject_len - cut);
strbuf_trim(msg);
@@ -244,7 +247,7 @@ static void print_commit(struct commit *commit, struct rev_info *revs)
oid_to_hex(&commit->object.oid), ctx.qry.vpath);
cgit_print_commit_decorations(commit);
html("</td><td class='col-author'>");
- cgit_open_filter(ctx.repo->email_filter, info->author_email, "log");
+ cgit_open_filter(ctx.repo->email_filter, info->author_email ? info->author_email : "", "log");
html_txt(info->author);
cgit_close_filter(ctx.repo->email_filter);
@@ -286,8 +289,9 @@ static void print_commit(struct commit *commit, struct rev_info *revs)
int msg_lines = ctx.qry.showmsg ? line_count(msgbuf.buf) : 0;
print_graph_padding(revs, &graphbuf, msg_lines);
- } else
+ } else {
html("<td></td>");
+ }
// Either way one cell is already on the row, so the message
// spans the remaining columns.
@@ -317,6 +321,27 @@ static const char *disambiguate_ref(const char *ref, int *must_free_result)
return ref;
}
+/*
+ * A range token is one revision, or two joined by two or three dots, and each
+ * side has to pass cgit_valid_rev. An empty side means HEAD to git.
+ */
+static int valid_range_token(const char *arg)
+{
+ const char *dots = strstr(arg, "..");
+ char *left;
+ int ok;
+
+ if (!dots)
+ return cgit_valid_rev(arg);
+ left = xstrndup(arg, dots - arg);
+ dots += 2;
+ if (*dots == '.')
+ dots++;
+ ok = (!*left || cgit_valid_rev(left)) && (!*dots || cgit_valid_rev(dots));
+ free(left);
+ return ok;
+}
+
static char *next_token(char **src)
{
char *token;
@@ -348,7 +373,7 @@ static void print_pager(struct rev_info *revs, int ofs, int cnt)
struct commit *more = get_revision(revs);
html("</table>\n");
- // A single page needs no pager, and an empty list is just noise.
+ // A single page needs no pager, and an empty list has nothing to page.
if (ofs <= 0 && !more)
return;
html("<ul class='pager'>");
@@ -378,7 +403,7 @@ static void print_pager(struct rev_info *revs, int ofs, int cnt)
void cgit_print_commit_decorations(struct commit *commit)
{
const struct name_decoration *deco;
- static char buf[1024];
+ const char *buf;
deco = get_name_decoration(&commit->object);
if (!deco)
@@ -388,12 +413,13 @@ void cgit_print_commit_decorations(struct commit *commit)
struct object_id oid_tag, peeled;
int is_annotated = 0;
- strlcpy(buf, prettify_refname(deco->name), sizeof(buf));
+ buf = prettify_refname(deco->name);
switch (deco->type) {
case DECORATION_NONE:
break;
case DECORATION_REF_LOCAL:
- cgit_log_link(buf, NULL, "branch-deco", buf, NULL, ctx.qry.vpath, 0, NULL, NULL, ctx.qry.showmsg, 0);
+ cgit_log_link(buf, NULL, "branch-deco", buf, NULL, ctx.qry.vpath, 0, NULL, NULL,
+ ctx.qry.showmsg, 0);
break;
case DECORATION_REF_TAG:
if (!refs_read_ref(get_main_ref_store(the_repository), deco->name, &oid_tag) &&
@@ -411,7 +437,8 @@ void cgit_print_commit_decorations(struct commit *commit)
);
break;
default:
- cgit_commit_link(buf, NULL, "deco", ctx.qry.head, oid_to_hex(&commit->object.oid), ctx.qry.vpath);
+ cgit_commit_link(buf, NULL, "deco", ctx.qry.head, oid_to_hex(&commit->object.oid),
+ ctx.qry.vpath);
break;
}
deco = deco->next;
@@ -435,11 +462,10 @@ void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern
if (!tip)
tip = ctx.qry.head;
tip = disambiguate_ref(tip, &must_free_tip);
- if (tip && tip[0] == '-') {
- // setup_revisions() reads a leading-dash argument as an
- // option, so a tip like "--output=<path>" would become a
- // request to write an arbitrary file. No valid ref or object
- // name begins with a dash, so refuse it.
+ // Checked here as well as in prepare_repo_cmd, since a caller can pass
+ // a tip of its own and setup_revisions reads a leading dash as an
+ // option.
+ if (tip && !cgit_valid_rev(tip)) {
cgit_print_error_page(400, "Bad Request", "Invalid revision");
if (must_free_tip)
free((char *)tip);
@@ -448,6 +474,16 @@ void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern
}
strvec_push(&rev_argv, tip);
+ // A leading colon makes git read the path as a pathspec with magic,
+ // which can end the request inside git.
+ if (path && path[0] == ':') {
+ cgit_print_error_page(400, "Bad Request", "Invalid path");
+ if (must_free_tip)
+ free((char *)tip);
+ strvec_clear(&rev_argv);
+ return;
+ }
+
if (grep && pattern && *pattern) {
pattern = xstrdup(pattern);
if (!strcmp(grep, "grep") || !strcmp(grep, "author") || !strcmp(grep, "committer")) {
@@ -455,16 +491,17 @@ void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern
} else if (!strcmp(grep, "range")) {
char *arg;
- // Each whitespace separated token is taken as a
- // revision expression only, since a leading dash
- // would reach setup_revisions as a rev-list option.
- // The tip pushed above goes away, since the range
- // supersedes it.
+ // Each whitespace separated token has to pass the same
+ // check as any other revision. The tip pushed above goes
+ // away, since the range supersedes it.
strvec_pop(&rev_argv);
while ((arg = next_token(&pattern))) {
- if (*arg == '-') {
- fprintf(stderr, "[cgit] Bad range expression: %s\n", arg);
- break;
+ if (!valid_range_token(arg)) {
+ cgit_print_error_page(400, "Bad Request", "Invalid revision");
+ if (must_free_tip)
+ free((char *)tip);
+ strvec_clear(&rev_argv);
+ return;
}
strvec_push(&rev_argv, arg);
}
@@ -504,6 +541,10 @@ void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern
load_ref_decorations(NULL, DECORATE_FULL_REFS);
rev.show_decorations = 1;
rev.grep_filter.ignore_case = 1;
+ // The search is literal. A pattern would run as a regular expression
+ // over every message in the history, and one with a backreference
+ // takes exponential time to match.
+ rev.grep_filter.pattern_type_option = GREP_PATTERN_TYPE_FIXED;
rev.diffopt.detect_rename = 1;
rev.diffopt.rename_limit = ctx.cfg.renamelimit;
@@ -511,7 +552,13 @@ void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern
DIFF_XDL_SET(&rev.diffopt, IGNORE_WHITESPACE);
compile_grep_patterns(&rev.grep_filter);
- prepare_revision_walk(&rev);
+ // A failed setup leaves the walk holding freed commits, so it must
+ // not be read from.
+ if (prepare_revision_walk(&rev)) {
+ cgit_print_error_page(500, "Internal Server Error", "Unable to read the history");
+ strvec_clear(&rev_argv);
+ return;
+ }
if (pager) {
cgit_print_layout_start();
@@ -549,14 +596,14 @@ void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern
if (ofs < 0)
ofs = 0;
- for (i = 0; i < ofs && (commit = get_revision(&rev)) != NULL; ) {
+ for (i = 0; i < ofs && (commit = get_revision(&rev)); ) {
if (should_show(commit, &rev))
i++;
release_commit_memory(the_repository->parsed_objects, commit);
commit->parents = NULL;
}
- for (i = 0; i < cnt && (commit = get_revision(&rev)) != NULL; ) {
+ for (i = 0; i < cnt && (commit = get_revision(&rev)); ) {
// Clearing the flag per commit keeps a commit from being
// diffed twice when the file or line columns are on.
counts_ready = 0;
@@ -570,7 +617,7 @@ void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern
if (pager) {
print_pager(&rev, ofs, cnt);
cgit_print_layout_end();
- } else if ((commit = get_revision(&rev)) != NULL) {
+ } else if ((commit = get_revision(&rev))) {
htmlf("<tr class='nohover'><td colspan='%d'>", columns);
cgit_log_link(
"[...]", NULL, NULL, ctx.qry.head, NULL, ctx.qry.vpath, 0,
@@ -579,8 +626,6 @@ void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern
html("</td></tr>\n");
}
- // The cast is safe because must_free_tip is only set for a string this
- // function allocated.
if (must_free_tip)
free((char *)tip);
}
diff --git a/source/ui-patch.c b/source/ui-patch.c
index baa87c9..1dcdfbe 100644
--- a/source/ui-patch.c
+++ b/source/ui-patch.c
@@ -35,7 +35,7 @@ static int resolve_range(const char *new_rev, const char *old_rev,
}
commit = lookup_commit_reference(the_repository, new_oid);
if (!commit) {
- cgit_print_error_page(404, "Not Found", "Bad commit reference: %s", new_rev);
+ cgit_print_error_page(404, "Not Found", "Not a commit: %s", new_rev);
return -1;
}
@@ -45,7 +45,7 @@ static int resolve_range(const char *new_rev, const char *old_rev,
return -1;
}
if (!lookup_commit_reference(the_repository, old_oid)) {
- cgit_print_error_page(404, "Not Found", "Bad commit reference: %s", old_rev);
+ cgit_print_error_page(404, "Not Found", "Not a commit: %s", old_rev);
return -1;
}
} else if (commit->parents && commit->parents->item) {
@@ -75,6 +75,13 @@ void cgit_print_patch(const char *new_rev, const char *old_rev, const char *pref
if (!new_rev)
new_rev = ctx.qry.head;
+ // A leading colon makes git read the path as a pathspec with magic,
+ // which can end the request inside git after the headers are out.
+ if (prefix && prefix[0] == ':') {
+ cgit_print_error_page(400, "Bad Request", "Invalid path");
+ return;
+ }
+
if (resolve_range(new_rev, old_rev, &new_oid, &old_oid))
return;
@@ -84,10 +91,6 @@ void cgit_print_patch(const char *new_rev, const char *old_rev, const char *pref
xsnprintf(rev_range, REV_RANGE_LEN, "%s..%s", oid_to_hex(&old_oid), oid_to_hex(&new_oid));
}
- ctx.page.mimetype = "text/plain";
- ctx.page.filename = cgit_fmt("%s.patch", rev_range);
- cgit_print_http_headers();
-
if (!ctx.cfg.enable_plain_email) {
rev_argv[FORMAT_ARG] =
"--format=format:From %H Mon Sep 17 00:00:00 2001%n"
@@ -101,11 +104,10 @@ void cgit_print_patch(const char *new_rev, const char *old_rev, const char *pref
rev.show_root_diff = 1;
rev.max_parents = 1;
rev.diffopt.output_format |= DIFF_FORMAT_DIFFSTAT | DIFF_FORMAT_PATCH | DIFF_FORMAT_SUMMARY;
+ // Allocated rather than formatted into cgit_fmt's fixed buffer, because
+ // the path comes from the request and a long one would abort the
+ // process.
if (prefix)
- // Allocated rather than formatted into cgit_fmt's fixed
- // buffer, because the path comes from the request and a long
- // one would abort the process here, with the headers for a
- // successful response already on the wire.
rev.diffopt.stat_sep = cgit_fmtalloc("(limited to '%s')\n\n", prefix);
setup_revisions(rev_argc, rev_argv, &rev, NULL);
// A single commit resolves to a range starting at its parent, so this
@@ -113,9 +115,18 @@ void cgit_print_patch(const char *new_rev, const char *old_rev, const char *pref
// emitting a patch for the whole history.
if (ctx.cfg.max_patch_count > 0)
rev.max_count = ctx.cfg.max_patch_count;
- prepare_revision_walk(&rev);
+ // A failed setup leaves the walk holding freed commits, so it must
+ // not be read from, and the headers wait until it has succeeded.
+ if (prepare_revision_walk(&rev)) {
+ cgit_print_error_page(500, "Internal Server Error", "Unable to read the history");
+ return;
+ }
+
+ ctx.page.mimetype = "text/plain";
+ ctx.page.filename = cgit_fmt("%s.patch", rev_range);
+ cgit_print_http_headers();
- while ((commit = get_revision(&rev)) != NULL) {
+ 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 version note below rather
diff --git a/source/ui-plain.c b/source/ui-plain.c
index 6b45637..959b1bf 100644
--- a/source/ui-plain.c
+++ b/source/ui-plain.c
@@ -15,10 +15,8 @@
#include "ui-plain.h"
#include "ui-shared.h"
-/*
- * A listing is opened by the entry that matched and closed only once the walk
- * is over, so the end of the page has to tell the three cases apart.
- */
+// A listing is opened by the entry that matched and closed only once the walk
+// is over, so the end of the page has to tell the three cases apart.
enum response {
RESPONSE_NONE,
RESPONSE_BLOB,
@@ -133,9 +131,9 @@ static void print_dir(const char *base, int baselen, const char *path)
// cgit_plain_link is asked for with a null path.
fullpath[len - 1] = 0;
slash = strrchr(fullpath, '/');
- if (slash)
+ if (slash) {
*(slash + 1) = 0;
- else {
+ } else {
free(fullpath);
fullpath = NULL;
}
@@ -199,6 +197,7 @@ static int walk_tree(const struct object_id *oid, struct strbuf *base,
static int dir_prefix_len(const char *path)
{
const char *slash = strrchr(path, '/');
+
if (slash)
return slash - path + 1;
return 0;
diff --git a/source/ui-refs.c b/source/ui-refs.c
index c375137..d89e3ea 100644
--- a/source/ui-refs.c
+++ b/source/ui-refs.c
@@ -16,11 +16,9 @@
#include "ui-refs.h"
#include "ui-shared.h"
-/*
- * The slice of a sorted ref list that one page shows, with end one past the
- * last row. size is what a full page holds, so it also decides whether the
- * list needs a pager.
- */
+// The slice of a sorted ref list that one page shows, with end one past the
+// last row. size is what a full page holds, so it also decides whether the
+// list needs a pager.
struct ref_page {
int size;
int start;
@@ -80,10 +78,12 @@ static void collect_branches(struct reflist *list)
static void print_branch_header(void)
{
- html("<tr class='nohover'><th class='left'>Branch</th>"
- "<th class='left'>Commit message</th>"
- "<th class='left col-author'>Author</th>"
- "<th colspan='2' class='left'>Age</th></tr>\n");
+ html(
+ "<tr class='nohover'><th class='left'>Branch</th>"
+ "<th class='left'>Commit message</th>"
+ "<th class='left col-author'>Author</th>"
+ "<th colspan='2' class='left'>Age</th></tr>\n"
+ );
}
static int print_branch(struct refinfo *ref)
@@ -100,7 +100,10 @@ static int print_branch(struct refinfo *ref)
if (ref->object->type == OBJ_COMMIT) {
cgit_commit_link(info->subject, NULL, NULL, name, NULL, NULL);
html("</td><td class='col-author'>");
- cgit_open_filter(ctx.repo->email_filter, info->author_email, "refs");
+ // A filter must not be handed a NULL argument.
+ cgit_open_filter(
+ ctx.repo->email_filter, info->author_email ? info->author_email : "", "refs"
+ );
html_txt(info->author);
cgit_close_filter(ctx.repo->email_filter);
html("</td><td colspan='2'>");
@@ -124,10 +127,12 @@ static void collect_tags(struct reflist *list)
static void print_tag_header(void)
{
- html("<tr class='nohover'><th class='left'>Tag</th>"
- "<th class='left'>Download</th>"
- "<th class='left col-author'>Author</th>"
- "<th colspan='2' class='left'>Age</th></tr>\n");
+ html(
+ "<tr class='nohover'><th class='left'>Tag</th>"
+ "<th class='left'>Download</th>"
+ "<th class='left col-author'>Author</th>"
+ "<th colspan='2' class='left'>Age</th></tr>\n"
+ );
}
static int print_tag(struct refinfo *ref)
@@ -162,7 +167,10 @@ static int print_tag(struct refinfo *ref)
cgit_close_filter(ctx.repo->email_filter);
}
} else if (ref->object->type == OBJ_COMMIT) {
- cgit_open_filter(ctx.repo->email_filter, ref->commit->author_email, "refs");
+ cgit_open_filter(
+ ctx.repo->email_filter, ref->commit->author_email ? ref->commit->author_email : "",
+ "refs"
+ );
html_txt(ref->commit->author);
cgit_close_filter(ctx.repo->email_filter);
}
@@ -201,7 +209,8 @@ static void print_ref_pager(int ofs, int pagesize, int count, const char *path)
}
htmlf("%d - %d of %d", ofs + 1, ofs + pagesize < count ? ofs + pagesize : count, count);
if (ofs + pagesize < count) {
- url = cgit_pageurl(ctx.qry.repo, cgit_fmt("refs/%s", path), cgit_fmt("ofs=%d", ofs + pagesize));
+ url = cgit_pageurl(ctx.qry.repo, cgit_fmt("refs/%s", path),
+ cgit_fmt("ofs=%d", ofs + pagesize));
html(" <a href='");
html_attr(url);
html("'>[next]</a>");
@@ -328,11 +337,11 @@ void cgit_print_refs(void)
cgit_print_layout_start();
html("<table class='list'>\n");
- if (ctx.qry.path && starts_with(ctx.qry.path, "heads"))
+ if (ctx.qry.path && starts_with(ctx.qry.path, "heads")) {
print_branches_page(ctx.cfg.max_ref_count);
- else if (ctx.qry.path && starts_with(ctx.qry.path, "tags"))
+ } else if (ctx.qry.path && starts_with(ctx.qry.path, "tags")) {
print_tags_page(ctx.cfg.max_ref_count);
- else {
+ } else {
cgit_print_branches(ctx.cfg.max_ref_count);
html("<tr class='nohover'><td colspan='5'></td></tr>\n");
cgit_print_tags(ctx.cfg.max_ref_count);
diff --git a/source/ui-repolist.c b/source/ui-repolist.c
index cd22afd..031cb0d 100644
--- a/source/ui-repolist.c
+++ b/source/ui-repolist.c
@@ -175,8 +175,10 @@ static int any_repos_visible(void)
return 0;
}
-// currenturl is passed in because it is the same for every heading and every
-// row, and working it out here would mean an allocation and a free per cell.
+/*
+ * currenturl is passed in because it is the same for every heading and every
+ * row, and working it out here would mean an allocation and a free per cell.
+ */
static void print_column_header(const char *title, const char *column, const char *currenturl)
{
htmlf("<th class='left col-%s'><a href='", column);
@@ -208,7 +210,7 @@ static int section_changed(const char *section, const char *last)
return 0;
if (!section || !last)
return 1;
- return strcmp(section, last) != 0;
+ return strcmp(section, last);
}
static void print_section_row(const char *section, int columns)
@@ -269,7 +271,8 @@ static void print_pager(int total, int pagelen, char *search, char *sort)
for (i = 0, ofs = 0; ofs < total; i++, ofs = i * pagelen) {
class = (ctx.qry.ofs == ofs) ? "current" : NULL;
html("<li>");
- cgit_index_link(cgit_fmt("[%d]", i + 1), cgit_fmt("Page %d", i + 1), class, search, sort, ofs, 0);
+ cgit_index_link(cgit_fmt("[%d]", i + 1), cgit_fmt("Page %d", i + 1), class, search, sort,
+ ofs, 0);
html("</li>");
}
html("</ul>\n");
diff --git a/source/ui-shared.c b/source/ui-shared.c
index bb786a6..25a84a6 100644
--- a/source/ui-shared.c
+++ b/source/ui-shared.c
@@ -27,9 +27,6 @@
#define MIN_TRUNCATE_LEN 15
#define ELLIPSIS_LEN 3
-// Lines of context git itself defaults to, which a link has no reason to name.
-#define DEFAULT_DIFF_CONTEXT 3
-
// Bounds the breadcrumbs, so that one request cannot turn into an unbounded
// row of links.
#define MAX_CRUMB_LEVELS 15
@@ -49,7 +46,7 @@ static const char *repo_basename(const char *reponame)
len = strlcpy(buf, reponame, sizeof(buf));
if (len >= sizeof(buf))
- die("repo_basename: truncated repository name '%s'", reponame);
+ die("Repository name too long: %s", reponame);
last = len - 1;
while (last && buf[last] == '/')
buf[last--] = '\0';
@@ -212,7 +209,7 @@ static void emit_diff_args(const char *delim)
{
if (ctx.qry.difftype) {
html(delim);
- htmlf("dt=%d", ctx.qry.difftype);
+ htmlf("dt=%d", (int)ctx.qry.difftype);
delim = "&amp;";
}
if (ctx.qry.context > 0 && ctx.qry.context != DEFAULT_DIFF_CONTEXT) {
@@ -395,9 +392,16 @@ static void add_clone_urls(void (*fn)(const char *), char *urls, char *suffix)
static const struct object_id *pinned_oid(void)
{
static struct object_id oid;
+ static int resolved, pinned;
struct object_id head_oid;
struct commit *commit;
+ // The chrome asks several times per page and the answer cannot change
+ // within a request.
+ if (resolved)
+ return pinned ? &oid : NULL;
+ resolved = 1;
+
if (!ctx.repo || !ctx.qry.has_oid || !ctx.qry.oid || !ctx.qry.head)
return NULL;
if (repo_get_oid(the_repository, ctx.qry.oid, &oid) ||
@@ -412,6 +416,7 @@ static const struct object_id *pinned_oid(void)
oidcpy(&oid, &commit->object.oid);
if (oideq(&oid, &head_oid))
return NULL;
+ pinned = 1;
return &oid;
}
@@ -463,6 +468,7 @@ static int print_branch_option(const struct reference *ref, void *data)
{
struct branch_option_data *opt = data;
const char *name = ref->name;
+
// The switcher runs on every page, so it is bounded like the refs list.
if (ctx.cfg.max_ref_count && opt->count >= ctx.cfg.max_ref_count)
return -1;
@@ -501,13 +507,15 @@ static void print_header(void)
cgit_index_link("index", NULL, NULL, NULL, NULL, 0, 1);
html(" : ");
cgit_summary_link(ctx.repo->name, NULL, NULL, NULL);
- } else
+ } else {
html_txt(ctx.cfg.root_title);
+ }
html("</h1>\n");
// A repository with no commits has no branches to list, so the
- // switcher would be an empty select next to a switch button.
- if (ctx.repo && ctx.env.authenticated && !ctx.empty_repo) {
+ // switcher would be an empty select next to a switch button, and an
+ // error raised before the head was resolved has nothing to select.
+ if (ctx.repo && ctx.env.authenticated && !ctx.empty_repo && ctx.qry.head) {
const struct object_id *pinned = pinned_oid();
// Only one option may carry selected, and a pinned commit
// outranks the branch it was reached from.
@@ -577,7 +585,8 @@ static void print_repo_tabs(void)
html("<ul>\n");
if (ctx.repo->readme.nr) {
html("<li>");
- reporevlink("about", "about", "About this repository", tab_class("about"), ctx.qry.head, NULL, NULL);
+ reporevlink("about", "about", "About this repository", tab_class("about"), ctx.qry.head,
+ NULL, NULL);
html("</li>\n");
}
html("<li>");
@@ -677,10 +686,14 @@ static void snapshot_link(const char *name, const char *title, const char *class
reporevlink("snapshot", name, title, class, head, rev, archivename);
}
-// The link is built out of the request in ctx.qry, so a caller that alters a
-// field of ctx.qry first gets a link differing in exactly that.
+/*
+ * The link is built out of the request in ctx.qry, so a caller that alters a
+ * field of ctx.qry first gets a link differing in exactly that.
+ */
static void self_link(const char *name, const char *title, const char *class)
{
+ const char *rev = ctx.qry.has_oid ? ctx.qry.oid : NULL;
+
if (!strcmp(ctx.qry.page, "repolist"))
cgit_index_link(name, title, class, ctx.qry.search, ctx.qry.sort, ctx.qry.ofs, 1);
else if (!strcmp(ctx.qry.page, "summary"))
@@ -688,32 +701,31 @@ static void self_link(const char *name, const char *title, const char *class)
else if (!strcmp(ctx.qry.page, "tag"))
cgit_tag_link(name, title, class, ctx.qry.has_oid ? ctx.qry.oid : ctx.qry.head);
else if (!strcmp(ctx.qry.page, "tree"))
- cgit_tree_link(name, title, class, ctx.qry.head, ctx.qry.has_oid ? ctx.qry.oid : NULL, ctx.qry.path);
+ cgit_tree_link(name, title, class, ctx.qry.head, rev, ctx.qry.path);
else if (!strcmp(ctx.qry.page, "plain"))
- cgit_plain_link(name, title, class, ctx.qry.head, ctx.qry.has_oid ? ctx.qry.oid : NULL, ctx.qry.path);
+ cgit_plain_link(name, title, class, ctx.qry.head, rev, ctx.qry.path);
else if (!strcmp(ctx.qry.page, "blame"))
- cgit_blame_link(name, title, class, ctx.qry.head, ctx.qry.has_oid ? ctx.qry.oid : NULL, ctx.qry.path);
+ cgit_blame_link(name, title, class, ctx.qry.head, rev, ctx.qry.path);
else if (!strcmp(ctx.qry.page, "log"))
cgit_log_link(
- name, title, class, ctx.qry.head, ctx.qry.has_oid ? ctx.qry.oid : NULL,
- ctx.qry.path, ctx.qry.ofs, ctx.qry.grep, ctx.qry.search,
- ctx.qry.showmsg, ctx.qry.follow
+ name, title, class, ctx.qry.head, rev, ctx.qry.path, ctx.qry.ofs, ctx.qry.grep,
+ ctx.qry.search, ctx.qry.showmsg, ctx.qry.follow
);
else if (!strcmp(ctx.qry.page, "commit"))
- cgit_commit_link(name, title, class, ctx.qry.head, ctx.qry.has_oid ? ctx.qry.oid : NULL, ctx.qry.path);
+ cgit_commit_link(name, title, class, ctx.qry.head, rev, ctx.qry.path);
else if (!strcmp(ctx.qry.page, "patch"))
- cgit_patch_link(name, title, class, ctx.qry.head, ctx.qry.has_oid ? ctx.qry.oid : NULL, ctx.qry.path);
+ cgit_patch_link(name, title, class, ctx.qry.head, rev, ctx.qry.path);
else if (!strcmp(ctx.qry.page, "refs"))
- cgit_refs_link(name, title, class, ctx.qry.head, ctx.qry.has_oid ? ctx.qry.oid : NULL, ctx.qry.path);
+ cgit_refs_link(name, title, class, ctx.qry.head, rev, ctx.qry.path);
else if (!strcmp(ctx.qry.page, "snapshot"))
- snapshot_link(name, title, class, ctx.qry.head, ctx.qry.has_oid ? ctx.qry.oid : NULL, ctx.qry.path);
+ snapshot_link(name, title, class, ctx.qry.head, rev, ctx.qry.path);
else if (!strcmp(ctx.qry.page, "diff"))
cgit_diff_link(name, title, class, ctx.qry.head, ctx.qry.oid, ctx.qry.oid2, ctx.qry.path);
else if (!strcmp(ctx.qry.page, "stats"))
cgit_stats_link(name, title, class, ctx.qry.head, ctx.qry.path);
else {
// A page name this switch does not know still gets a plain
- // repolink, which covers any simple page added later.
+ // repolink.
repolink(title, class, ctx.qry.page, ctx.qry.head, ctx.qry.path);
html("'>");
html_txt(name);
@@ -746,7 +758,9 @@ static void print_path_crumbs(char *path)
ctx.qry.path = old_path;
}
-// A reverse memchr, which glibc has as memrchr but macOS and the BSDs do not.
+/*
+ * A reverse memchr, which glibc has as memrchr but macOS and the BSDs do not.
+ */
static const char *find_last_char(const char *start, const char *end, int c)
{
while (end > start) {
@@ -759,6 +773,7 @@ static const char *find_last_char(const char *start, const char *end, int c)
static void vprint_error(const char *fmt, va_list ap)
{
va_list cp;
+
html("<div class='error'>");
va_copy(cp, ap);
html_vtxtf(fmt, cp);
@@ -769,6 +784,7 @@ static void vprint_error(const char *fmt, va_list ap)
void cgit_print_error(const char *fmt, ...)
{
va_list ap;
+
va_start(ap, fmt);
vprint_error(fmt, ap);
va_end(ap);
@@ -819,15 +835,16 @@ char *cgit_currentfullurl(void)
memcpy(query + 1, orig_query, len + 1);
query[0] = '?';
match = query;
- while ((match = strstr(match, "url=")) != NULL) {
+ while ((match = strstr(match, "url="))) {
if (match[-1] == '?' || match[-1] == '&') {
const char *next = strchr(match, '&');
if (next)
memmove(match, next + 1, strlen(next));
else
match[0] = '\0';
- } else
- ++match;
+ } else {
+ match++;
+ }
}
if (!query[1])
query[0] = '\0';
@@ -845,6 +862,7 @@ char *cgit_currentfullurl(void)
const char *cgit_loginurl(void)
{
static const char *login_url;
+
if (!login_url)
login_url = cgit_fmtalloc("%s?p=login", root_url());
return login_url;
@@ -868,7 +886,7 @@ char *cgit_fileurl(const char *reponame, const char *pagename, const char *filen
// and each sink escapes the whole string for wherever it lands.
if (ctx.cfg.virtual_root) {
strbuf_addf(&sb, "%s%s/%s/%s", ctx.cfg.virtual_root, reponame, pagename,
- (filename ? filename:""));
+ filename ? filename : "");
delim = "?";
} else {
strbuf_addf(&sb, "?url=%s/%s/%s", reponame, pagename, (filename ? filename : ""));
@@ -974,12 +992,20 @@ void cgit_commit_link(const char *name, const char *title, const char *class,
html("'>");
if (name && name[0] != '\0') {
if (ctx.cfg.max_msg_len >= MIN_TRUNCATE_LEN && strlen(name) > (size_t)ctx.cfg.max_msg_len) {
- html_ntxt(name, ctx.cfg.max_msg_len - ELLIPSIS_LEN);
+ size_t len = ctx.cfg.max_msg_len - ELLIPSIS_LEN;
+
+ // A cut inside a multibyte character would leave an
+ // invalid sequence on the page.
+ while (len > 0 && (name[len] & 0xC0) == 0x80)
+ len--;
+ html_ntxt(name, len);
html("...");
- } else
+ } else {
html_txt(name);
- } else
+ }
+ } else {
html_txt("(no commit message)");
+ }
html("</a>");
}
@@ -1151,11 +1177,9 @@ static struct cgit_repo *repo_serving_url(const char *url)
return best;
}
-/*
- * Hosts whose repository and commit pages follow a known form, so an ssh
- * url can still be offered as a browser link. kernel.org runs cgit, which
- * keeps the .git suffix in its own page urls.
- */
+// Hosts whose repository and commit pages follow a known form, so an ssh url
+// can still be offered as a browser link. kernel.org runs cgit, which keeps
+// the .git suffix in its own page urls.
static const struct forge {
const char *host;
const char *commit_seg;
@@ -1336,6 +1360,7 @@ void cgit_submodule_link(const char *class, char *path, const char *rev)
struct date_mode cgit_date_mode(enum date_mode_type type)
{
static struct date_mode mode;
+
mode.type = type;
mode.local = ctx.cfg.local_time;
return mode;
@@ -1382,6 +1407,7 @@ void cgit_print_age(time_t t, int tz, time_t max_relative)
void cgit_print_http_headers(void)
{
+ ctx.page.headers_sent = 1;
if (ctx.env.no_http && !strcmp(ctx.env.no_http, "1"))
return;
@@ -1396,7 +1422,7 @@ void cgit_print_http_headers(void)
else if (ctx.page.mimetype)
htmlf("Content-Type: %s\n", ctx.page.mimetype);
if (ctx.page.size)
- htmlf("Content-Length: %zd\n", ctx.page.size);
+ htmlf("Content-Length: %zu\n", ctx.page.size);
if (ctx.page.filename) {
html("Content-Disposition: inline; filename=\"");
html_header_arg_in_quotes(ctx.page.filename);
@@ -1539,6 +1565,7 @@ void cgit_print_docend(void)
void cgit_print_error_page(int code, const char *msg, const char *fmt, ...)
{
va_list ap;
+
va_start(ap, fmt);
cgit_vprint_error_page(code, msg, fmt, ap);
va_end(ap);
@@ -1550,9 +1577,21 @@ void cgit_vprint_error_page(int code, const char *msg, const char *fmt, va_list
// holding it. A request naming a commit not yet pushed would cache its
// 404 under the never-expiring static ttl and keep serving it after
// the push, so the error is rendered straight to the visitor instead.
+ html_flush();
cache_abandon_fill();
+ // Once the status line is out the error can only join the body under
+ // way, and the page that hit it carries on to its own end.
+ if (ctx.page.headers_sent) {
+ vprint_error(fmt, ap);
+ return;
+ }
ctx.page.status = code;
ctx.page.statusmsg = msg;
+ // A page that had already chosen another type, such as an image
+ // served through the about page, still answers its error as html.
+ ctx.page.mimetype = "text/html";
+ ctx.page.charset = PAGE_ENCODING;
+ ctx.page.filename = NULL;
cgit_print_layout_start();
vprint_error(fmt, ap);
cgit_print_layout_end();
@@ -1581,7 +1620,9 @@ void cgit_add_clone_urls(void (*fn)(const char *))
static char *http_clone_url;
static int http_clone_url_scanned;
-// Scheme names compare case-insensitively.
+/*
+ * Scheme names compare case-insensitively.
+ */
static int is_http_url(const char *url)
{
return istarts_with(url, "http://") || istarts_with(url, "https://");
@@ -1745,47 +1786,60 @@ void cgit_print_filemode(unsigned short mode)
html_fileperm(mode);
}
-/*
- * One tag's claim on a snapshot stem, the stem being the tag name with any
- * leading v or V removed. Claims are counted over the tag list itself rather
- * than by looking refs up by name, because a loose ref lookup on a
- * case-insensitive filesystem finds v1.2 when asked for V1.2 and would call
- * every tag still stored loose ambiguous.
- */
-struct snapshot_stem {
- const char *tag;
- const char *stem;
- int claimants;
- int is_tag;
-};
+// The tag names and their stems, a stem being the name with any leading v or V
+// removed, gathered once per request. Claims on a stem are counted over the
+// tag list itself rather than by looking refs up by name, because a loose ref
+// lookup on a case-insensitive filesystem finds v1.2 when asked for V1.2 and
+// would call every tag still stored loose ambiguous.
+static struct string_list tag_names = STRING_LIST_INIT_DUP;
+static struct string_list tag_stems = STRING_LIST_INIT_DUP;
+static int tags_collected;
-static int count_stem_claimants(const struct reference *ref, void *data)
+static int collect_tag(const struct reference *ref, void *data)
{
- struct snapshot_stem *probe = data;
const char *name = ref->name;
- if (!strcmp(name, probe->tag))
- probe->is_tag = 1;
+ string_list_append(&tag_names, name);
if (name[0] == 'v' || name[0] == 'V')
name++;
- if (!strcmp(name, probe->stem))
- probe->claimants++;
+ string_list_append(&tag_stems, name);
return 0;
}
+/*
+ * Whether ref is a tag whose stem no other tag shares.
+ */
+static int stem_is_unique(const char *ref)
+{
+ struct string_list_item *stem;
+ size_t i;
+
+ if (!tags_collected) {
+ tags_collected = 1;
+ refs_for_each_tag_ref(get_main_ref_store(the_repository), collect_tag, NULL);
+ string_list_sort(&tag_names);
+ string_list_sort(&tag_stems);
+ }
+ if (!string_list_has_string(&tag_names, ref))
+ return 0;
+ stem = string_list_lookup(&tag_stems, ref + 1);
+ if (!stem)
+ return 0;
+ // The lookup lands on any one of equal entries, so both neighbours
+ // are checked.
+ i = stem - tag_stems.items;
+ return (i == 0 || strcmp(tag_stems.items[i - 1].string, stem->string)) &&
+ (i + 1 == tag_stems.nr || strcmp(tag_stems.items[i + 1].string, stem->string));
+}
+
static void compose_snapshot_prefix(struct strbuf *filename, const char *base, const char *ref)
{
// A tag named v1.2 or V1.2 gives its snapshot the prettier name 1.2,
// but only where dropping the letter cannot land two different tags on
// one name, so that a snapshot can still be traced back to the tag it
// was made from.
- if ((ref[0] == 'v' || ref[0] == 'V') && isdigit((unsigned char)ref[1])) {
- struct snapshot_stem probe = { .tag = ref, .stem = ref + 1 };
-
- refs_for_each_tag_ref(get_main_ref_store(the_repository), count_stem_claimants, &probe);
- if (probe.is_tag && probe.claimants == 1)
- ref++;
- }
+ if ((ref[0] == 'v' || ref[0] == 'V') && isdigit((unsigned char)ref[1]) && stem_is_unique(ref))
+ ref++;
strbuf_addf(filename, "%s-%s", base, ref);
}
@@ -1815,7 +1869,10 @@ void cgit_print_snapshot_links(const struct cgit_repo *repo, const char *ref, co
html(" (");
snapshot_link("sig", NULL, NULL, NULL, NULL, filename.buf);
html(")");
- } else if (starts_with(f->suffix, ".tar") && cgit_snapshot_get_sig(ref, &cgit_snapshot_formats[0])) {
+ } else if (
+ starts_with(f->suffix, ".tar") &&
+ cgit_snapshot_get_sig(ref, &cgit_snapshot_formats[0])
+ ) {
// A compressed tarball offers the signature made for
// the plain tar it expands to, which is the first
// format in the table.
@@ -1841,7 +1898,7 @@ void cgit_set_title_from_path(const char *path)
// The path is read from the end so that the title names the file first
// and the directories it sits in after it.
last_slash = path + strlen(path);
- while ((slash = find_last_char(path, last_slash, '/')) != NULL) {
+ while ((slash = find_last_char(path, last_slash, '/'))) {
strbuf_add(&sb, slash + 1, last_slash - slash - 1);
strbuf_addstr(&sb, " < ");
last_slash = slash;
diff --git a/source/ui-shared.h b/source/ui-shared.h
index e2d2db9..4809bc1 100644
--- a/source/ui-shared.h
+++ b/source/ui-shared.h
@@ -29,7 +29,9 @@ extern char *cgit_fileurl(const char *reponame, const char *pagename, const char
const char *query);
extern char *cgit_pageurl(const char *reponame, const char *pagename, const char *query);
-// Call fn once for every URL this repository can be cloned from.
+/*
+ * Call fn once for every URL this repository can be cloned from.
+ */
extern void cgit_add_clone_urls(void (*fn)(const char *));
/*
@@ -39,7 +41,8 @@ extern void cgit_add_clone_urls(void (*fn)(const char *));
*/
extern void cgit_index_link(const char *name, const char *title, const char *class,
const char *pattern, const char *sort, int ofs, int always_root);
-extern void cgit_summary_link(const char *name, const char *title, const char *class, const char *head);
+extern void cgit_summary_link(const char *name, const char *title, const char *class,
+ const char *head);
extern void cgit_tag_link(const char *name, const char *title, const char *class, const char *tag);
extern void cgit_tree_link(const char *name, const char *title, const char *class,
const char *head, const char *rev, const char *path);
@@ -60,7 +63,9 @@ extern void cgit_diff_link(const char *name, const char *title, const char *clas
const char *head, const char *new_rev, const char *old_rev, const char *path);
extern void cgit_stats_link(const char *name, const char *title, const char *class,
const char *head, const char *path);
-// Names the object by type and id, abbreviated unless full is set.
+/*
+ * Names the object by type and id, abbreviated unless full is set.
+ */
extern void cgit_object_link(struct object *obj, int full);
extern void cgit_submodule_link(const char *class, char *path, const char *rev);
@@ -88,10 +93,13 @@ extern void cgit_print_error_page(int code, const char *msg, const char *fmt, ..
extern void cgit_vprint_error_page(int code, const char *msg, const char *fmt, va_list ap);
extern void cgit_print_pageheader(void);
extern void cgit_print_filemode(unsigned short mode);
-extern void cgit_print_snapshot_links(const struct cgit_repo *repo, const char *ref, const char *separator);
+extern void cgit_print_snapshot_links(const struct cgit_repo *repo, const char *ref,
+ const char *separator);
-// The name a snapshot of this repository is downloaded under, which is the
-// configured prefix or the last component of the repository URL.
+/*
+ * The name a snapshot of this repository is downloaded under, which is the
+ * configured prefix or the last component of the repository URL.
+ */
extern const char *cgit_snapshot_prefix(const struct cgit_repo *repo);
/*
@@ -102,6 +110,8 @@ extern const char *cgit_snapshot_prefix(const struct cgit_repo *repo);
*/
extern void cgit_add_hidden_formfields(int incl_head, int incl_search, const char *page);
-// Put path in front of the page title, its last component first.
+/*
+ * Put path in front of the page title, its last component first.
+ */
extern void cgit_set_title_from_path(const char *path);
#endif // CGIT_UI_SHARED_H
diff --git a/source/ui-snapshot.c b/source/ui-snapshot.c
index a9bf858..5f2d69d 100644
--- a/source/ui-snapshot.c
+++ b/source/ui-snapshot.c
@@ -15,6 +15,7 @@
#include "cgit.h"
#include "filter.h"
#include "html.h"
+#include "shared.h"
#include "ui-shared.h"
#include "ui-snapshot.h"
@@ -61,6 +62,31 @@ static int write_zip_archive(const char *hex, const char *prefix)
return write_archive_format("--format=zip", hex, prefix);
}
+static int program_on_path(const char *program)
+{
+ const char *path = getenv("PATH");
+ struct strbuf full = STRBUF_INIT;
+ int found = 0;
+
+ if (!path)
+ return 0;
+ while (!found) {
+ const char *end = strchrnul(path, ':');
+
+ strbuf_reset(&full);
+ strbuf_add(&full, path, end - path);
+ if (!full.len)
+ strbuf_addch(&full, '.');
+ strbuf_addf(&full, "/%s", program);
+ found = !access(full.buf, X_OK);
+ if (!*end)
+ break;
+ path = end + 1;
+ }
+ strbuf_release(&full);
+ return found;
+}
+
static int write_compressed_tar_archive(const char *hex, const char *prefix, char *argv[])
{
struct cgit_exec_filter filter;
@@ -76,24 +102,28 @@ static int write_compressed_tar_archive(const char *hex, const char *prefix, cha
static int write_tar_gzip_archive(const char *hex, const char *prefix)
{
char *argv[] = { "gzip", "-n", NULL };
+
return write_compressed_tar_archive(hex, prefix, argv);
}
static int write_tar_bzip2_archive(const char *hex, const char *prefix)
{
char *argv[] = { "bzip2", NULL };
+
return write_compressed_tar_archive(hex, prefix, argv);
}
static int write_tar_lzip_archive(const char *hex, const char *prefix)
{
char *argv[] = { "lzip", NULL };
+
return write_compressed_tar_archive(hex, prefix, argv);
}
static int write_tar_xz_archive(const char *hex, const char *prefix)
{
char *argv[] = { "xz", NULL };
+
return write_compressed_tar_archive(hex, prefix, argv);
}
@@ -102,20 +132,21 @@ static int write_tar_zstd_archive(const char *hex, const char *prefix)
// Single-threaded like the other compressors, since -T0 would let one
// request pin every core.
char *argv[] = { "zstd", NULL };
+
return write_compressed_tar_archive(hex, prefix, argv);
}
// ui-shared.c reads entry zero as the tar whose signature stands in for every
// tar variant, so this order cannot change.
const struct cgit_snapshot_format cgit_snapshot_formats[] = {
- { ".tar", "application/x-tar", write_tar_archive },
- { ".tar.gz", "application/gzip", write_tar_gzip_archive },
- { ".tar.bz2", "application/x-bzip2", write_tar_bzip2_archive },
- { ".tar.lz", "application/x-lzip", write_tar_lzip_archive },
- { ".tar.xz", "application/x-xz", write_tar_xz_archive },
- { ".tar.zst", "application/zstd", write_tar_zstd_archive },
- { ".zip", "application/zip", write_zip_archive },
- { NULL }
+ { ".tar", "application/x-tar", write_tar_archive, NULL },
+ { ".tar.gz", "application/gzip", write_tar_gzip_archive, "gzip" },
+ { ".tar.bz2", "application/x-bzip2", write_tar_bzip2_archive, "bzip2" },
+ { ".tar.lz", "application/x-lzip", write_tar_lzip_archive, "lzip" },
+ { ".tar.xz", "application/x-xz", write_tar_xz_archive, "xz" },
+ { ".tar.zst", "application/zstd", write_tar_zstd_archive, "zstd" },
+ { ".zip", "application/zip", write_zip_archive, NULL },
+ { 0 }
};
// Each tree is read the first time a signature for that format is asked for,
@@ -142,7 +173,10 @@ static int resolves(const char *rev)
{
struct object_id oid;
- return repo_get_oid(the_repository, rev, &oid) == 0;
+ // The name goes no further than this lookup, so a leading dash is
+ // fine here, while the wider syntax is refused as in cgit_valid_rev.
+ return !check_refname_format(rev, REFNAME_ALLOW_ONELEVEL) &&
+ !repo_get_oid(the_repository, rev, &oid);
}
static const char *ref_from_filename(const struct cgit_repo *repo, const char *filename,
@@ -197,14 +231,23 @@ static int send_snapshot(const struct cgit_snapshot_format *format, const char *
return 1;
}
if (!lookup_commit_reference(the_repository, &oid)) {
- cgit_print_error_page(400, "Bad Request", "Not a commit reference: %s", hex);
+ cgit_print_error_page(404, "Not Found", "Not a commit: %s", hex);
+ return 1;
+ }
+ // A compressor that cannot run only shows up as a broken pipe once
+ // the tar is under way, so it is looked for while a status can still
+ // say so.
+ if (format->program && !program_on_path(format->program)) {
+ cgit_print_error_page(500, "Internal Server Error", "Unable to run %s", format->program);
return 1;
}
ctx.page.mimetype = xstrdup(format->mimetype);
ctx.page.filename = xstrdup(filename);
cgit_print_http_headers();
init_archivers();
- format->write_func(hex, prefix);
+ // The archiver reads its arguments as a command line, so it gets the
+ // resolved id and never the name.
+ format->write_func(oid_to_hex(&oid), prefix);
return 0;
}
@@ -321,6 +364,12 @@ void cgit_print_snapshot(const char *head, const char *hex, const char *filename
return;
}
+ // The name becomes the prefix of every member of the archive, and a
+ // revision expression such as :/pattern could carry a path in it.
+ if (!hex && dwim && strchr(filename, '/')) {
+ cgit_print_error_page(404, "Not Found", "Not found");
+ return;
+ }
if (!hex && dwim) {
hex = ref_from_filename(ctx.repo, filename, f);
if (!hex) {
diff --git a/source/ui-snapshot.h b/source/ui-snapshot.h
index c594168..412807e 100644
--- a/source/ui-snapshot.h
+++ b/source/ui-snapshot.h
@@ -17,6 +17,8 @@ struct cgit_snapshot_format {
const char *suffix;
const char *mimetype;
write_archive_fn_t write_func;
+ // The compressor the writer runs, or NULL when it needs none.
+ const char *program;
};
// Terminated by an entry with a NULL suffix.
@@ -32,7 +34,9 @@ extern unsigned cgit_snapshot_format_bit(const struct cgit_snapshot_format *f);
*/
extern int cgit_parse_snapshots_mask(const char *str);
-// The detached signature stored for this format under refs/notes, if any.
+/*
+ * The detached signature stored for this format under refs/notes, if any.
+ */
extern const struct object_id *cgit_snapshot_get_sig(
const char *ref, const struct cgit_snapshot_format *f);
diff --git a/source/ui-ssdiff.c b/source/ui-ssdiff.c
index 2ea4751..ad543f4 100644
--- a/source/ui-ssdiff.c
+++ b/source/ui-ssdiff.c
@@ -14,10 +14,6 @@
#include "ui-shared.h"
#include "ui-ssdiff.h"
-// The stylesheet sets no tab-size and tabs are expanded here rather than left
-// to the browser, so this has to be the width a browser would pick on its own.
-#define TAB_WIDTH 8
-
// One line held back until its run ends, owning the copy taken of it.
struct deferred_line {
int line_no;
@@ -90,13 +86,13 @@ static char *longest_common_subsequence(const char *old_line, const char *new_li
while (i < old_len && j < new_len) {
if (old_line[i] == new_line[j]) {
lcs[pos] = old_line[i];
- pos += 1;
- i += 1;
- j += 1;
+ pos++;
+ i++;
+ j++;
} else if (lcs_table[i + 1][j] >= lcs_table[i][j + 1]) {
- i += 1;
+ i++;
} else {
- j += 1;
+ j++;
}
}
@@ -143,9 +139,9 @@ static void print_line_with_lcs(const char *class, const char *line, const char
for (i = 0; i < len; i++) {
if (in_common) {
- if (line[i] == lcs[matched])
- matched += 1;
- else {
+ if (line[i] == lcs[matched]) {
+ matched++;
+ } else {
in_common = 0;
flush_run(&run);
htmlf("<span class='%s'>", class);
@@ -154,7 +150,7 @@ static void print_line_with_lcs(const char *class, const char *line, const char
in_common = 1;
flush_run(&run);
html("</span>");
- matched += 1;
+ matched++;
}
strbuf_addch(&run, line[i]);
}
@@ -202,10 +198,11 @@ static void print_row(const char *class, int old_line_no, char *old_line,
if (old_line_no > 0) {
print_lineno_cell(cgit_get_current_old_file(), old_rev_oid, old_line_no);
htmlf("<td class='%s'>", class);
- } else if (old_line)
+ } else if (old_line) {
htmlf("<td class='lineno'></td><td class='%s'>", class);
- else
+ } else {
htmlf("<td class='lineno'></td><td class='%s_dark'>", class);
+ }
if (old_line) {
if (lcs)
print_line_with_lcs("del", old_line, lcs);
@@ -217,10 +214,11 @@ static void print_row(const char *class, int old_line_no, char *old_line,
if (new_line_no > 0) {
print_lineno_cell(cgit_get_current_new_file(), new_rev_oid, new_line_no);
htmlf("<td class='%s'>", class);
- } else if (new_line)
+ } else if (new_line) {
htmlf("<td class='lineno'></td><td class='%s'>", class);
- else
+ } else {
htmlf("<td class='lineno'></td><td class='%s_dark'>", class);
+ }
if (new_line) {
if (lcs)
print_line_with_lcs("add", new_line, lcs);
@@ -264,7 +262,7 @@ static int count_deferred(struct deferred_line *item)
int count = 0;
while (item) {
- count += 1;
+ count++;
item = item->next;
}
return count;
@@ -342,9 +340,9 @@ static int hunk_start_line(const char *hunk, char marker)
long line_no;
p = strchr(hunk, marker);
- if (p == NULL)
+ if (!p)
return 0;
- p += 1;
+ p++;
line_no = strtol(p, NULL, 10);
if (line_no < 0 || line_no > INT_MAX)
return 0;
@@ -383,14 +381,14 @@ void cgit_ssdiff_line_cb(char *line, int len)
if (line[0] == ' ') {
print_deferred_lines();
print_row("ctx", current_old_line, line, current_new_line, line, 0);
- current_old_line += 1;
- current_new_line += 1;
+ current_old_line++;
+ current_new_line++;
} else if (line[0] == '+') {
defer_line(&deferred_new, &deferred_new_last, line, current_new_line);
- current_new_line += 1;
+ current_new_line++;
} else if (line[0] == '-') {
defer_line(&deferred_old, &deferred_old_last, line, current_old_line);
- current_old_line += 1;
+ current_old_line++;
} else if (line[0] == '@') {
html("<tr><td colspan='4' class='hunk'>");
html_txt(line);
diff --git a/source/ui-stats.c b/source/ui-stats.c
index a04b189..3960b14 100644
--- a/source/ui-stats.c
+++ b/source/ui-stats.c
@@ -17,11 +17,9 @@
#define DEFAULT_AUTHOR_ROWS 10
-/*
- * One author's share of the window. periods is keyed by the label of a
- * period, with the commit count stored in the util field itself rather than
- * behind another allocation.
- */
+// One author's share of the window. periods is keyed by the label of a period,
+// with the commit count stored in the util field itself rather than behind
+// another allocation.
struct authorstat {
long total;
struct string_list periods;
@@ -144,10 +142,8 @@ static char *pretty_year(struct tm *tm)
return cgit_fmt("%d", tm->tm_year + 1900);
}
-/*
- * The order runs from the finest window to the coarsest, because a repository
- * caps the page by storing an index into this table as its max-stats.
- */
+// The order runs from the finest window to the coarsest, because a repository
+// caps the page by storing an index into this table as its max-stats.
static const struct cgit_period periods[] = {
{'w', "week", 4, trunc_week, dec_week, inc_week, pretty_week},
{'m', "month", 4, trunc_month, dec_month, inc_month, pretty_month},
@@ -157,10 +153,13 @@ static const struct cgit_period periods[] = {
static void window_start(const struct cgit_period *period, struct tm *tm)
{
- time_t now;
+ // Read once, so the buckets and the column labels agree on the window
+ // even when the clock crosses a period boundary between them.
+ static time_t now;
int i;
- time(&now);
+ if (!now)
+ time(&now);
gmtime_r(&now, tm);
period->trunc(tm);
for (i = 1; i < period->count; i++)
@@ -175,7 +174,6 @@ static void add_commit(struct string_list *authors, struct commitinfo *info,
char *name, *label;
struct tm date;
time_t when;
- uintptr_t *count;
// A commit can lack an author header, so fall back rather than
// handing xstrdup a NULL.
@@ -194,11 +192,11 @@ static void add_commit(struct string_list *authors, struct commitinfo *info,
return;
period->trunc(&date);
label = xstrdup(period->pretty(&date));
+ // The pointer field holds the count itself.
bucket = string_list_insert(&stats->periods, label);
- count = (uintptr_t *)&bucket->util;
- if (*count)
+ if (bucket->util)
free(label);
- (*count)++;
+ bucket->util = (void *)((uintptr_t)bucket->util + 1);
stats->total++;
}
@@ -213,10 +211,11 @@ static struct string_list collect_stats(const struct cgit_period *period)
struct string_list authors;
struct rev_info rev;
struct commit *commit;
- // setup_revisions reads the entries after the double dash up to a
- // NULL, past the count, so the sentinel has to stay even when the
- // path fills the slot before it.
- const char *argv[] = {NULL, ctx.qry.head, NULL, NULL, NULL};
+ // setup_revisions reads these the way main does, skipping the first
+ // entry, and reads past the count up to a NULL after the double dash,
+ // so the sentinel has to stay even when the path fills the slot before
+ // it.
+ const char *argv[] = { NULL, ctx.qry.head, NULL, NULL, NULL };
int argc = 2;
time_t since;
struct tm tm;
@@ -235,16 +234,19 @@ static struct string_list collect_stats(const struct cgit_period *period)
rev.max_parents = 1;
rev.verbose_header = 1;
rev.show_root_diff = 0;
- // setup_revisions reads argv the way main does and ignores the first
- // entry, so the head to walk sits at argv[1].
setup_revisions(argc, argv, &rev, NULL);
// Prune the walk to the displayed window instead of traversing the
// whole history and discarding older commits. The check below still
// bounds the period edge exactly.
rev.max_age = since;
- prepare_revision_walk(&rev);
memset(&authors, 0, sizeof(authors));
- while ((commit = get_revision(&rev)) != NULL) {
+ // A failed setup leaves the walk holding freed commits, so it must
+ // not be read from.
+ if (prepare_revision_walk(&rev)) {
+ cgit_print_error_page(500, "Internal Server Error", "Unable to read the history");
+ return authors;
+ }
+ while ((commit = get_revision(&rev))) {
struct commitinfo *info = cgit_parse_commit(commit);
if ((time_t)info->committer_date >= since)
@@ -352,7 +354,7 @@ static void print_authors(struct string_list *authors, int max_rows, const struc
if (!bucket)
html("<td>0</td>");
else {
- htmlf("<td>%lu</td>", (uintptr_t)bucket->util);
+ htmlf("<td>%ju</td>", (uintmax_t)(uintptr_t)bucket->util);
total += (uintptr_t)bucket->util;
}
}
@@ -360,7 +362,8 @@ static void print_authors(struct string_list *authors, int max_rows, const struc
}
if (rows < authors->nr)
- print_summary_row(authors, rows, authors->nr - rows, "Others (%ld)", "left", "", "sum", labels);
+ print_summary_row(authors, rows, authors->nr - rows, "Others (%ld)", "left", "", "sum",
+ labels);
print_summary_row(authors, 0, authors->nr, "Total", "total", "sum", "sum", labels);
html("</table>\n");
@@ -380,7 +383,8 @@ static void print_options_form(const struct cgit_period *period, int top)
html("<tr><td class='label'>Period:</td>");
html("<td class='ctrl'><select name='period'>");
for (i = 0; i < choices; i++)
- html_option(cgit_fmt("%c", periods[i].code), periods[i].name, cgit_fmt("%c", period->code));
+ html_option(cgit_fmt("%c", periods[i].code), periods[i].name,
+ cgit_fmt("%c", period->code));
html("</select></td></tr>\n");
}
html("<tr><td class='label'>Authors:</td>");
diff --git a/source/ui-stats.h b/source/ui-stats.h
index feee741..325e082 100644
--- a/source/ui-stats.h
+++ b/source/ui-stats.h
@@ -38,8 +38,10 @@ struct cgit_period {
*/
extern int cgit_find_stats_period(const char *expr, const struct cgit_period **period);
-// The name of the period at a one based index, or an empty string when the
-// index names no period.
+/*
+ * The name of the period at a one based index, or an empty string when the
+ * index names no period.
+ */
extern const char *cgit_find_stats_periodname(int idx);
extern void cgit_show_stats(void);
diff --git a/source/ui-summary.c b/source/ui-summary.c
index ea8f783..da530e5 100644
--- a/source/ui-summary.c
+++ b/source/ui-summary.c
@@ -71,10 +71,10 @@ static int path_within(const char *base, const char *path)
static char *resolve_about_path(const char *filename, const char *ref, const char *path)
{
char *copy, *base_dir, *full_path;
- char *resolved_base = NULL, *resolved_full = NULL;
+ char *resolved_base = NULL, *resolved_full = NULL, *resolved_repo = NULL;
// dirname is allowed to write into its argument and to return a
- // pointer into it, so base_dir borrows from a copy we keep alive.
+ // pointer into it, so base_dir borrows from a copy freed at the end.
copy = xstrdup(filename);
base_dir = dirname(copy);
if (!strcmp(base_dir, ".") || !strcmp(base_dir, "..")) {
@@ -83,13 +83,21 @@ static char *resolve_about_path(const char *filename, const char *ref, const cha
return NULL;
}
full_path = xstrdup(path);
- } else
+ } else {
full_path = cgit_fmtalloc("%s/%s", base_dir, path);
+ }
+ // A readme in the repository directory, or above it, would open the
+ // repository's own files, its config and hooks among them.
if (!ref) {
resolved_base = realpath(base_dir, NULL);
resolved_full = realpath(full_path, NULL);
- if (!resolved_base || !resolved_full || !path_within(resolved_base, resolved_full)) {
+ resolved_repo = realpath(ctx.repo->path, NULL);
+ if (
+ !resolved_base || !resolved_full || !resolved_repo ||
+ path_within(resolved_base, resolved_repo) ||
+ !path_within(resolved_base, resolved_full)
+ ) {
free(full_path);
full_path = NULL;
}
@@ -98,6 +106,7 @@ static char *resolve_about_path(const char *filename, const char *ref, const cha
free(copy);
free(resolved_base);
free(resolved_full);
+ free(resolved_repo);
return full_path;
}
diff --git a/source/ui-summary.h b/source/ui-summary.h
index 676cc47..0f61da5 100644
--- a/source/ui-summary.h
+++ b/source/ui-summary.h
@@ -1,6 +1,6 @@
/*
- * Declarations for the two pages a repository opens with, the summary of its
- * refs and recent commits and the about page built from its readme. cmd.c
+ * The summary and about pages, the two a repository opens with, one listing
+ * its refs and recent commits and the other built from its readme. cmd.c
* reaches both, and it hands the about page whatever path follows the page
* name in the url, so a readme can link to a file beside it.
*/
diff --git a/source/ui-tag.c b/source/ui-tag.c
index 56862bb..6811e0c 100644
--- a/source/ui-tag.c
+++ b/source/ui-tag.c
@@ -131,6 +131,16 @@ static void print_lightweight_tag(const char *revname, struct object *obj)
cgit_print_layout_end();
}
+/*
+ * A tag pointing at another tag links the inner one by object id, since no ref
+ * names it.
+ */
+static int tag_by_id(const char *name, struct object_id *oid)
+{
+ return !get_oid_hex(name, oid) &&
+ odb_read_object_info(the_repository->objects, oid, NULL) == OBJ_TAG;
+}
+
void cgit_print_tag(char *revname)
{
struct strbuf fullref = STRBUF_INIT;
@@ -141,13 +151,14 @@ void cgit_print_tag(char *revname)
revname = ctx.qry.head;
strbuf_addf(&fullref, "refs/tags/%s", revname);
- if (repo_get_oid(the_repository, fullref.buf, &oid)) {
+ if (repo_get_oid(the_repository, fullref.buf, &oid) && !tag_by_id(revname, &oid)) {
cgit_print_error_page(404, "Not Found", "Bad tag reference: %s", revname);
goto cleanup;
}
obj = parse_object(the_repository, &oid);
if (!obj) {
- cgit_print_error_page(500, "Internal Server Error", "Bad object id: %s", oid_to_hex(&oid));
+ cgit_print_error_page(500, "Internal Server Error", "Unable to read object %s",
+ oid_to_hex(&oid));
goto cleanup;
}
if (obj->type == OBJ_TAG)
diff --git a/source/ui-tree.c b/source/ui-tree.c
index aab448e..4d829fe 100644
--- a/source/ui-tree.c
+++ b/source/ui-tree.c
@@ -20,6 +20,9 @@
#define HEXDUMP_ROW_HALF (HEXDUMP_ROW_BYTES / 2)
#define HEXDUMP_GAP_WIDTH 4
+// How many single-child directories a listing row follows into one path.
+#define MAX_DIR_CHAIN_LEVELS 15
+
enum walk_state {
WALK_LOOKING,
WALK_LISTING,
@@ -41,10 +44,8 @@ struct walk_tree_context {
size_t entries_nr, entries_alloc;
};
-/*
- * The count runs past one as soon as a second entry or a file turns up, which
- * ends the descent.
- */
+// The count runs past one as soon as a second entry or a file turns up, which
+// ends the descent.
struct only_child {
struct strbuf *path;
struct object_id oid;
@@ -56,20 +57,24 @@ struct only_child {
* The anchors are handed over in batches rather than a write per line, and
* never built whole, which would come to several times the blob's size.
*/
+static void add_linenumber(struct strbuf *numbers, unsigned long lineno)
+{
+ strbuf_addf(numbers, "<a id='n%lu' href='#n%lu'>%lu</a>\n", lineno, lineno, lineno);
+}
+
static void print_linenumbers(const char *buf, unsigned long size)
{
- const char *numberfmt = "<a id='n%1$d' href='#n%1$d'>%1$d</a>\n";
struct strbuf numbers = STRBUF_INIT;
unsigned long lineno = 0, idx = 0;
if (size) {
- strbuf_addf(&numbers, numberfmt, ++lineno);
+ add_linenumber(&numbers, ++lineno);
// The newline that ends the last line must not open a line of
// its own, so the final byte is left out of the scan.
while (idx < size - 1) {
if (buf[idx] == '\n') {
- strbuf_addf(&numbers, numberfmt, ++lineno);
+ add_linenumber(&numbers, ++lineno);
if (numbers.len >= HTML_BATCH) {
html_raw(numbers.buf, numbers.len);
strbuf_reset(&numbers);
@@ -136,7 +141,7 @@ static void print_binary_buffer(char *buf, unsigned long size)
for (idx = 0; idx < HEXDUMP_ROW_BYTES && offset + idx < size; idx++) {
int gap = idx == HEXDUMP_ROW_HALF ? HEXDUMP_GAP_WIDTH : 1;
- strbuf_addf(&row, "%*s%02x", gap, "", buf[idx] & 0xff);
+ strbuf_addf(&row, "%*s%02x", gap, "", (unsigned char)buf[idx]);
}
strbuf_addstr(&row, " </td><td class='hex'>");
html_raw(row.buf, row.len);
@@ -166,7 +171,7 @@ static bool print_object(const struct object_id *oid, const char *path,
type = odb_read_object_info(the_repository->objects, oid, &size);
if (type == OBJ_BAD) {
- cgit_print_error_page(404, "Not Found", "Bad object name: %s", oid_to_hex(oid));
+ cgit_print_error_page(404, "Not Found", "Bad object id: %s", oid_to_hex(oid));
return false;
}
@@ -180,7 +185,8 @@ static bool print_object(const struct object_id *oid, const char *path,
buf = odb_read_object(the_repository->objects, oid, &type, &size);
if (!buf) {
- cgit_print_error_page(500, "Internal Server Error", "Error reading object %s", oid_to_hex(oid));
+ cgit_print_error_page(500, "Internal Server Error", "Unable to read object %s",
+ oid_to_hex(oid));
return false;
}
// buffer_is_binary only sniffs the front of the blob, and a NUL past
@@ -239,10 +245,13 @@ static void print_dir_chain(const struct object_id *oid, char *name, char *rev,
struct pathspec paths = {
.nr = 0
};
+ int levels = 0;
oidcpy(&child.oid, oid);
- while (child.count == 1) {
+ // Each level links the whole path so far, so a chain of thousands of
+ // single directories would make one row quadratic in size.
+ while (child.count == 1 && levels++ < MAX_DIR_CHAIN_LEVELS) {
cgit_tree_link(name, NULL, "ls-dir", ctx.qry.head, rev, fullpath->buf);
tree = lookup_tree(the_repository, &child.oid);
@@ -305,17 +314,22 @@ static void print_ls_row(const struct object_id *oid, const char *pathname,
strbuf_addf(&class, " %s", ext + 1);
cgit_tree_link(name, NULL, class.buf, ctx.qry.head, walk->rev, fullpath.buf);
}
- if (S_ISLNK(mode)) {
+ // A target longer than any path is no link, and reading it would pull
+ // a blob of any size into memory.
+ if (S_ISLNK(mode) && size <= PATH_MAX) {
html(" -> ");
buf = odb_read_object(the_repository->objects, oid, &type, &size);
if (!buf) {
- htmlf("Error reading object: %s", oid_to_hex(oid));
+ htmlf("Unable to read object %s", oid_to_hex(oid));
goto cleanup;
}
strbuf_addbuf(&linkpath, &fullpath);
strbuf_addf(&linkpath, "/../%s", buf);
- strbuf_normalize_path(&linkpath);
- cgit_tree_link(buf, NULL, class.buf, ctx.qry.head, walk->rev, linkpath.buf);
+ // A target climbing above the tree root has nothing to link to.
+ if (strbuf_normalize_path(&linkpath))
+ html_txt(buf);
+ else
+ cgit_tree_link(buf, NULL, class.buf, ctx.qry.head, walk->rev, linkpath.buf);
free(buf);
strbuf_release(&linkpath);
}
@@ -455,8 +469,10 @@ static int walk_tree(const struct object_id *oid, struct strbuf *base,
return 0;
}
-// Either argument may be null, in which case the head of the current query
-// stands in for the revision and the listing starts at the root of the tree.
+/*
+ * Either argument may be null, in which case the head of the current query
+ * stands in for the revision and the listing starts at the root of the tree.
+ */
void cgit_print_tree(const char *rev, char *path)
{
struct object_id oid;
@@ -482,18 +498,18 @@ void cgit_print_tree(const char *rev, char *path)
rev = ctx.qry.head;
if (repo_get_oid(the_repository, rev, &oid)) {
- cgit_print_error_page(404, "Not Found", "Invalid revision name: %s", rev);
+ cgit_print_error_page(404, "Not Found", "Bad object id: %s", rev);
return;
}
commit = lookup_commit_reference(the_repository, &oid);
if (!commit || repo_parse_commit(the_repository, commit)) {
- cgit_print_error_page(404, "Not Found", "Invalid commit reference: %s", rev);
+ cgit_print_error_page(404, "Not Found", "Not a commit: %s", rev);
return;
}
walk.rev = xstrdup(rev);
- if (path == NULL) {
+ if (!path) {
ls_tree(get_commit_tree_oid(commit), &walk);
goto cleanup;
}
@@ -502,10 +518,11 @@ void cgit_print_tree(const char *rev, char *path)
if (walk.state == WALK_LISTING) {
ls_flush(&walk);
ls_tail();
- } else if (walk.state == WALK_BLOB_SHOWN)
+ } else if (walk.state == WALK_BLOB_SHOWN) {
cgit_print_layout_end();
- else if (walk.state == WALK_LOOKING)
+ } else if (walk.state == WALK_LOOKING) {
cgit_print_error_page(404, "Not Found", "Path not found");
+ }
// WALK_ERROR_SHOWN is left alone, since the error page print_object
// put out is already complete.