diff options
Diffstat (limited to 'source/ui-shared.c')
| -rw-r--r-- | source/ui-shared.c | 1491 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
1 file changed, 763 insertions, 728 deletions
diff --git a/source/ui-shared.c b/source/ui-shared.c index 23da286..938f292 100644 --- a/source/ui-shared.c +++ b/source/ui-shared.c @@ -1,119 +1,69 @@ -/* ui-shared.c: common web output functions - * - * Copyright (C) 2006-2017 cgit Development Team <cgit@lists.zx2c4.com> - * - * Licensed under GNU General Public License v2 - * (see LICENSE.txt for full license text) +/* + * The output every cgit page has in common. A link from one page to another is + * built here because only this file knows whether a site addresses a request + * as a path below a virtual root or as a query string, and the two are spelled + * differently down to the separator between arguments. The headers, the + * document head, the header block, the footer, the error page and the age + * column are here so that a page handler is left with only its own body to + * produce. */ #define USE_THE_REPOSITORY_VARIABLE #include "cgit.h" -#include "ui-shared.h" -#include "cmd.h" #include "html.h" -#include "version.h" +#include "shared.h" +#include "ui-shared.h" +#include "ui-snapshot.h" -static const char cgit_doctype[] = -"<!DOCTYPE html>\n"; +#define DEFAULT_HTTP_PORT 80 -static char *http_date(time_t t) -{ - static char day[][4] = - {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; - static char month[][4] = - {"Jan", "Feb", "Mar", "Apr", "May", "Jun", - "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; - struct tm tm; - gmtime_r(&t, &tm); - return cgit_fmt("%s, %02d %s %04d %02d:%02d:%02d GMT", day[tm.tm_wday], - tm.tm_mday, month[tm.tm_mon], 1900 + tm.tm_year, - tm.tm_hour, tm.tm_min, tm.tm_sec); -} +#define SHORT_HASH_LEN 10 -void cgit_print_error(const char *fmt, ...) -{ - va_list ap; - va_start(ap, fmt); - cgit_vprint_error(fmt, ap); - va_end(ap); -} +// A max-msg-len below this leaves too little of a commit subject to be worth +// truncating, and the ellipsis is paid for out of that same budget. +#define MIN_TRUNCATE_LEN 15 +#define ELLIPSIS_LEN 3 -void cgit_vprint_error(const char *fmt, va_list ap) -{ - va_list cp; - html("<div class='error'>"); - va_copy(cp, ap); - html_vtxtf(fmt, cp); - va_end(cp); - html("</div>\n"); -} +// Lines of context git itself defaults to, which a link has no reason to name. +#define DEFAULT_DIFF_CONTEXT 3 -const char *cgit_httpscheme(void) -{ - if (ctx.env.https && !strcmp(ctx.env.https, "on")) - return "https://"; - else - return "http://"; -} - -char *cgit_hosturl(void) -{ - if (ctx.env.http_host) - return xstrdup(ctx.env.http_host); - if (!ctx.env.server_name) - return NULL; - if (!ctx.env.server_port || atoi(ctx.env.server_port) == 80) - return xstrdup(ctx.env.server_name); - return cgit_fmtalloc("%s:%s", ctx.env.server_name, ctx.env.server_port); -} +// Bounds the breadcrumbs, so that one request cannot turn into an unbounded +// row of links. +#define MAX_CRUMB_LEVELS 15 -char *cgit_currenturl(void) -{ - const char *root = cgit_rooturl(); +// cgit_fmt exits the request rather than truncate, so a path is only worked +// into a tab title while it still leaves room to spare in that buffer. +#define MAX_TITLE_PATH_LEN 256 - if (!ctx.qry.url) - return xstrdup(root); - if (root[0] && root[strlen(root) - 1] == '/') - return cgit_fmtalloc("%s%s", root, ctx.qry.url); - return cgit_fmtalloc("%s/%s", root, ctx.qry.url); -} - -char *cgit_currentfullurl(void) +static const char *repo_basename(const char *reponame) { - const char *root = cgit_rooturl(); - const char *orig_query = ctx.env.query_string ? ctx.env.query_string : ""; - size_t len = strlen(orig_query); - char *query = xmalloc(len + 2), *start_url, *ret; + // The result lives in a static buffer, so it is only good until the + // next call. + static char buf[1024]; + const char *slash; + size_t len; + int last; - /* Remove all url=... parts from query string */ - memcpy(query + 1, orig_query, len + 1); - query[0] = '?'; - start_url = query; - while ((start_url = strstr(start_url, "url=")) != NULL) { - if (start_url[-1] == '?' || start_url[-1] == '&') { - const char *end_url = strchr(start_url, '&'); - if (end_url) - memmove(start_url, end_url + 1, strlen(end_url)); - else - start_url[0] = '\0'; - } else - ++start_url; + len = strlcpy(buf, reponame, sizeof(buf)); + if (len >= sizeof(buf)) + die("repo_basename: truncated repository name '%s'", reponame); + last = len - 1; + while (last && buf[last] == '/') + buf[last--] = '\0'; + if (last >= 3 && starts_with(&buf[last - 3], ".git")) { + last -= 3; + buf[last--] = '\0'; } - if (!query[1]) - query[0] = '\0'; - - if (!ctx.qry.url) - ret = cgit_fmtalloc("%s%s", root, query); - else if (root[0] && root[strlen(root) - 1] == '/') - ret = cgit_fmtalloc("%s%s%s", root, ctx.qry.url, query); - else - ret = cgit_fmtalloc("%s/%s%s", root, ctx.qry.url, query); - free(query); - return ret; + while (last && buf[last] == '/') + buf[last--] = '\0'; + slash = strrchr(buf, '/'); + if (slash) + return ++slash; + return buf; } -const char *cgit_rooturl(void) +static const char *root_url(void) { if (ctx.cfg.virtual_root) return ctx.cfg.virtual_root; @@ -121,92 +71,13 @@ const char *cgit_rooturl(void) return ctx.cfg.script_name; } -const char *cgit_loginurl(void) -{ - static const char *login_url; - if (!login_url) - login_url = cgit_fmtalloc("%s?p=login", cgit_rooturl()); - return login_url; -} - -char *cgit_repourl(const char *reponame) -{ - if (ctx.cfg.virtual_root) - return cgit_fmtalloc("%s%s/", ctx.cfg.virtual_root, reponame); - else - return cgit_fmtalloc("?r=%s", reponame); -} - -char *cgit_fileurl(const char *reponame, const char *pagename, - const char *filename, const char *query) -{ - struct strbuf sb = STRBUF_INIT; - const char *delim; - - if (ctx.cfg.virtual_root) { - strbuf_addf(&sb, "%s%s/%s/%s", ctx.cfg.virtual_root, reponame, - pagename, (filename ? filename:"")); - delim = "?"; - } else { - strbuf_addf(&sb, "?url=%s/%s/%s", reponame, pagename, - (filename ? filename : "")); - delim = "&"; - } - if (query) - strbuf_addf(&sb, "%s%s", delim, query); - return strbuf_detach(&sb, NULL); -} - -char *cgit_pageurl(const char *reponame, const char *pagename, - const char *query) -{ - return cgit_fileurl(reponame, pagename, NULL, query); -} - -const char *cgit_repobasename(const char *reponame) -{ - /* I assume we don't need to store more than one repo basename */ - static char rvbuf[1024]; - int p; - const char *rv; - size_t len; - - len = strlcpy(rvbuf, reponame, sizeof(rvbuf)); - if (len >= sizeof(rvbuf)) - die("cgit_repobasename: truncated repository name '%s'", reponame); - p = len - 1; - /* strip trailing slashes */ - while (p && rvbuf[p] == '/') - rvbuf[p--] = '\0'; - /* strip trailing .git */ - if (p >= 3 && starts_with(&rvbuf[p-3], ".git")) { - p -= 3; - rvbuf[p--] = '\0'; - } - /* strip more trailing slashes if any */ - while (p && rvbuf[p] == '/') - rvbuf[p--] = '\0'; - /* find last slash in the remaining string */ - rv = strrchr(rvbuf, '/'); - if (rv) - return ++rv; - return rvbuf; -} - -const char *cgit_snapshot_prefix(const struct cgit_repo *repo) -{ - if (repo->snapshot_prefix) - return repo->snapshot_prefix; - - return cgit_repobasename(repo->url); -} - -static void site_url(const char *page, const char *search, const char *sort, int ofs, int always_root) +static void site_url(const char *page, const char *search, const char *sort, + int ofs, int always_root) { const char *delim = "?"; if (always_root || page) - html_attr(cgit_rooturl()); + html_attr(root_url()); else { char *currenturl = cgit_currenturl(); html_attr(currenturl); @@ -236,7 +107,8 @@ static void site_url(const char *page, const char *search, const char *sort, int } static void site_link(const char *page, const char *name, const char *title, - const char *class, const char *search, const char *sort, int ofs, int always_root) + const char *class, const char *search, const char *sort, + int ofs, int always_root) { html("<a"); if (title) { @@ -256,14 +128,15 @@ static void site_link(const char *page, const char *name, const char *title, html("</a>"); } -void cgit_index_link(const char *name, const char *title, const char *class, - const char *pattern, const char *sort, int ofs, int always_root) -{ - site_link(NULL, name, title, class, pattern, sort, ofs, always_root); -} - -static const char *repolink(const char *title, const char *class, const char *page, - const char *head, const char *path) +/* + * Open an anchor and write the part of its href naming the repository, the + * page and the path, leaving the href quote open for the caller to add its own + * arguments and close. The return is the delimiter to put before the first of + * those, which is still a question mark unless a query string has been opened. + */ +static const char *repolink(const char *title, const char *class, + const char *page, const char *head, + const char *path) { const char *delim = "?"; @@ -313,6 +186,17 @@ static const char *repolink(const char *title, const char *class, const char *pa return cgit_fmt("%s", delim); } +static const char *emit_rev_arg(const char *delim, const char *rev) +{ + if (rev && ctx.qry.head && strcmp(rev, ctx.qry.head)) { + html(delim); + html("id="); + html_url_arg(rev); + return "&"; + } + return delim; +} + static void reporevlink(const char *page, const char *name, const char *title, const char *class, const char *head, const char *rev, const char *path) @@ -320,16 +204,637 @@ static void reporevlink(const char *page, const char *name, const char *title, const char *delim; delim = repolink(title, class, page, head, path); - if (rev && ctx.qry.head != NULL && strcmp(rev, ctx.qry.head)) { - html(delim); - html("id="); - html_url_arg(rev); - } + emit_rev_arg(delim, rev); html("'>"); html_txt(name); html("</a>"); } +static void emit_diff_args(const char *delim) +{ + if (ctx.qry.difftype) { + html(delim); + htmlf("dt=%d", ctx.qry.difftype); + delim = "&"; + } + if (ctx.qry.context > 0 && ctx.qry.context != DEFAULT_DIFF_CONTEXT) { + html(delim); + html("context="); + htmlf("%d", ctx.qry.context); + delim = "&"; + } + if (ctx.qry.ignorews) { + html(delim); + html("ignorews=1"); + delim = "&"; + } + if (ctx.qry.follow) { + html(delim); + html("follow=1"); + } +} + +static struct string_list_item *lookup_path(struct string_list *list, + const char *path) +{ + struct string_list_item *item; + + while (path && path[0]) { + if ((item = string_list_lookup(list, path))) + return item; + if (!(path = strchr(path, '/'))) + break; + path++; + } + return NULL; +} + +/* + * A repository can supply the module-link template through a repo-local + * cgitrc, so the %s placeholders are expanded here instead of the template + * being handed to printf, where a surplus conversion would read past the + * argument list. + */ +static void emit_module_link(const char *tmpl, const char **args, int nargs) +{ + struct strbuf sb = STRBUF_INIT; + int used = 0; + + while (*tmpl) { + if (*tmpl != '%') { + strbuf_addch(&sb, *tmpl++); + continue; + } + tmpl++; + if (*tmpl == '%') { + strbuf_addch(&sb, '%'); + tmpl++; + } else if (*tmpl == 's' && used < nargs) { + strbuf_addstr(&sb, args[used++]); + tmpl++; + } else { + strbuf_addch(&sb, '%'); + } + } + html_attr(sb.buf); + strbuf_release(&sb); +} + +static struct date_mode configured_date_mode(void) +{ + struct date_mode mode = ctx.cfg.date_mode; + + if (ctx.cfg.local_time) + mode.local = 1; + return mode; +} + +static void print_abs_date(time_t t, int tz) +{ + html("<span title='"); + html_attr(show_date(t, tz, cgit_date_mode(DATE_ISO8601))); + html("'>"); + html_txt(show_date(t, tz, configured_date_mode())); + html("</span>"); +} + +static void print_rel_date(time_t t, int tz, double count, const char *class, + const char *suffix) +{ + htmlf("<span class='%s' data-ut='%" PRIu64 "' title='", class, (uint64_t)t); + html_attr(show_date(t, tz, cgit_date_mode(DATE_ISO8601))); + htmlf("'>%.0f %s</span>", count, suffix); +} + +static char *http_date(time_t t) +{ + static char day[][4] = + {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; + static char month[][4] = + {"Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; + struct tm tm; + gmtime_r(&t, &tm); + return cgit_fmt("%s, %02d %s %04d %02d:%02d:%02d GMT", day[tm.tm_wday], + tm.tm_mday, month[tm.tm_mon], 1900 + tm.tm_year, + tm.tm_hour, tm.tm_min, tm.tm_sec); +} + +static void print_rel_vcs_link(const char *url) +{ + html("<link rel='vcs-git' href='"); + html_attr(url); + html("' title='"); + html_attr(ctx.repo->name); + html(" Git repository'/>\n"); +} + +static int emit_css_link(struct string_list_item *item, void *fallback) +{ + // An empty css entry is how a site turns the stylesheet off. + if (item && *item->string == '\0') + return 0; + + html("<link rel='stylesheet' type='text/css' href='"); + if (item) + html_attr(item->string); + else + html_attr((const char *)fallback); + html("'/>\n"); + + return 0; +} + +static int emit_js_link(struct string_list_item *item, void *fallback) +{ + // An empty js entry is how a site turns the script off. + if (item && *item->string == '\0') + return 0; + + html("<script type='text/javascript' src='"); + if (item) + html_attr(item->string); + else + html_attr((const char *)fallback); + html("'></script>\n"); + + return 0; +} + +static void add_clone_urls(void (*fn)(const char *), char *urls, char *suffix) +{ + struct strbuf **url_list = strbuf_split_str(urls, ' ', 0); + int i; + + for (i = 0; url_list[i]; i++) { + strbuf_rtrim(url_list[i]); + if (url_list[i]->len == 0) + continue; + if (suffix && *suffix) + strbuf_addf(url_list[i], "/%s", suffix); + fn(url_list[i]->buf); + } + + strbuf_list_free(url_list); +} + +static int print_branch_option(const struct reference *ref, void *counter) +{ + int *count = counter; + 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 && *count >= ctx.cfg.max_ref_count) + return -1; + (*count)++; + html_option(name, name, ctx.qry.head); + return 0; +} + +static void print_header(void) +{ + const char *logo = NULL, *logo_link = NULL; + + if (ctx.repo && ctx.repo->logo && *ctx.repo->logo) + logo = ctx.repo->logo; + else + logo = ctx.cfg.logo; + if (ctx.repo && ctx.repo->logo_link && *ctx.repo->logo_link) + logo_link = ctx.repo->logo_link; + else + logo_link = ctx.cfg.logo_link; + + html("<header id='header'>\n"); + if (logo && *logo) { + html("<a class='logo' href='"); + if (logo_link && *logo_link) + html_attr(logo_link); + else + html_attr(root_url()); + html("'><img src='"); + html_attr(logo); + html("' alt='cgit logo'/></a>\n"); + } + + html("<h1 class='main'>"); + if (ctx.repo) { + cgit_index_link("index", NULL, NULL, NULL, NULL, 0, 1); + html(" : "); + cgit_summary_link(ctx.repo->name, NULL, NULL, NULL); + } 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) { + html("<form class='branch-switch' method='get'>\n"); + cgit_add_hidden_formfields(0, 1, ctx.qry.page); + html("<select name='h' aria-label='Branch' data-autosubmit='1'>\n"); + int branch_count = 0; + refs_for_each_branch_ref(get_main_ref_store(the_repository), + print_branch_option, &branch_count); + if (ctx.repo->enable_remote_branches) + refs_for_each_remote_ref(get_main_ref_store(the_repository), + print_branch_option, &branch_count); + html("</select> "); + html("<input type='submit' value='switch'/>"); + html("</form>\n"); + } + + if (ctx.repo) { + html("<p class='sub desc'>"); + html_txt(ctx.repo->desc); + html("</p>\n"); + html("<p class='sub owner'>"); + html_txt(ctx.repo->owner); + html("</p>\n"); + } else if (ctx.cfg.root_desc) { + html("<p class='sub desc'>"); + html_txt(ctx.cfg.root_desc); + html("</p>\n"); + } + + html("<button type='button' class='theme-toggle' hidden " + "aria-label='Toggle color theme'></button>\n"); + html("</header>\n"); +} + +static const char *tab_class(const char *page) +{ + if (!ctx.qry.page) + return NULL; + + return strcmp(ctx.qry.page, page) ? NULL : "active"; +} + +static const char *tab_title(const char *format, const char *fallback, + const char *path) +{ + if (!path || !*path || strlen(path) > MAX_TITLE_PATH_LEN) + return fallback; + return cgit_fmt(format, path); +} + +static void print_repo_tabs(void) +{ + const char *vpath = ctx.qry.vpath; + + html("<ul>\n"); + if (ctx.repo->readme.nr) { + html("<li>"); + reporevlink("about", "about", "About this repository", + tab_class("about"), ctx.qry.head, NULL, NULL); + html("</li>\n"); + } + html("<li>"); + cgit_summary_link("summary", "Repository summary", tab_class("summary"), + ctx.qry.head); + html("</li>\n<li>"); + cgit_refs_link("refs", "Branches and tags", tab_class("refs"), + ctx.qry.head, ctx.qry.oid, NULL); + html("</li>\n<li>"); + cgit_log_link("log", + tab_title("Commit history of %s", "Commit history", vpath), + tab_class("log"), ctx.qry.head, + NULL, ctx.qry.vpath, 0, NULL, NULL, + ctx.qry.showmsg, ctx.qry.follow); + html("</li>\n<li>"); + if (ctx.qry.page && !strcmp(ctx.qry.page, "blame")) + cgit_blame_link("blame", + tab_title("Line-by-line blame of %s", "Blame", vpath), + tab_class("blame"), ctx.qry.head, + ctx.qry.oid, ctx.qry.vpath); + else + cgit_tree_link("tree", + tab_title("Browse the tree at %s", "Browse the file tree", vpath), + tab_class("tree"), ctx.qry.head, + ctx.qry.oid, ctx.qry.vpath); + html("</li>\n<li>"); + cgit_commit_link("commit", "The current commit", tab_class("commit"), + ctx.qry.head, ctx.qry.oid, ctx.qry.vpath); + html("</li>\n<li>"); + cgit_diff_link("diff", + tab_title("Changes to %s", "Changes in the current commit", vpath), + tab_class("diff"), ctx.qry.head, + ctx.qry.oid, ctx.qry.oid2, ctx.qry.vpath); + html("</li>\n"); + if (ctx.repo->enable_stats) { + html("<li>"); + cgit_stats_link("stats", "Commit activity", tab_class("stats"), + ctx.qry.head, ctx.qry.vpath); + html("</li>\n"); + } + if (ctx.repo->homepage) { + html("<li><a class='external' href='"); + html_attr(ctx.repo->homepage); + html("'>homepage</a></li>\n"); + } + html("</ul>\n"); +} + +static void print_repo_search(void) +{ + html("<form class='search' method='get' action='"); + if (ctx.cfg.virtual_root) { + char *fileurl = cgit_fileurl(ctx.qry.repo, "log", + ctx.qry.vpath, NULL); + html_url_path(fileurl); + free(fileurl); + } + html("'>\n"); + cgit_add_hidden_formfields(1, 0, "log"); + html("<select name='qt' aria-label='Search type'>\n"); + html_option("grep", "log msg", ctx.qry.grep); + html_option("author", "author", ctx.qry.grep); + html_option("committer", "committer", ctx.qry.grep); + html_option("range", "range", ctx.qry.grep); + html("</select>\n"); + html("<input class='txt' type='search' name='q' aria-label='Search' value='"); + html_attr(ctx.qry.search); + html("'/>\n"); + html("<input type='submit' value='search'/>\n"); + html("</form>\n"); +} + +static void print_site_tabs(void) +{ + html("<ul>\n<li>"); + site_link(NULL, "index", "Repository index", tab_class("repolist"), + NULL, NULL, 0, 1); + html("</li>\n"); + if (ctx.cfg.root_readme) { + html("<li>"); + site_link("about", "about", "About this site", + tab_class("about"), NULL, NULL, 0, 1); + html("</li>\n"); + } + html("</ul>\n"); +} + +static void print_site_search(void) +{ + char *currenturl = cgit_currenturl(); + + html("<form class='search' method='get' action='"); + html_attr(currenturl); + html("'>\n"); + html("<input class='txt' type='search' name='q' aria-label='Search' value='"); + html_attr(ctx.qry.search); + html("'/>\n"); + html("<input type='submit' value='search'/>\n"); + html("</form>\n"); + free(currenturl); +} + +// 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 snapshot_link(const char *name, const char *title, const char *class, + const char *head, const char *rev, + const char *archivename) +{ + reporevlink("snapshot", name, title, class, head, rev, archivename); +} + +static void self_link(const char *name, const char *title, const char *class) +{ + 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")) + cgit_summary_link(name, title, class, ctx.qry.head); + 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); + 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); + 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); + 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); + 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); + 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); + 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); + 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); + 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 { + repolink(title, class, ctx.qry.page, ctx.qry.head, ctx.qry.path); + html("><!-- cgit_self_link() doesn't know how to make link for page '"); + html_txt(ctx.qry.page); + html("' -->"); + html_txt(name); + html("</a>"); + } +} + +static void print_path_crumbs(char *path) +{ + char *old_path = ctx.qry.path; + char *start = path, *slash, *end = path + strlen(path); + int levels = 0; + + ctx.qry.path = NULL; + self_link("root", NULL, NULL); + // self_link reads the path out of ctx.qry, so terminating the buffer at + // each slash is what makes a crumb link to the prefix it stands for. + ctx.qry.path = start = path; + while (start < end) { + if (!(slash = strchr(start, '/')) || levels > MAX_CRUMB_LEVELS) + slash = end; + *slash = '\0'; + html_txt("/"); + self_link(start, NULL, NULL); + if (slash < end) + *slash = '/'; + start = slash + 1; + ++levels; + } + ctx.qry.path = old_path; +} + +// 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) { + if (*--end == c) + return end; + } + return NULL; +} + +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); + va_end(cp); + html("</div>\n"); +} + +void cgit_print_error(const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + vprint_error(fmt, ap); + va_end(ap); +} + +const char *cgit_httpscheme(void) +{ + if (ctx.env.https && !strcmp(ctx.env.https, "on")) + return "https://"; + else + return "http://"; +} + +char *cgit_hosturl(void) +{ + if (ctx.env.http_host) + return xstrdup(ctx.env.http_host); + if (!ctx.env.server_name) + return NULL; + if (!ctx.env.server_port || + atoi(ctx.env.server_port) == DEFAULT_HTTP_PORT) + return xstrdup(ctx.env.server_name); + return cgit_fmtalloc("%s:%s", ctx.env.server_name, ctx.env.server_port); +} + +char *cgit_currenturl(void) +{ + const char *root = root_url(); + + if (!ctx.qry.url) + return xstrdup(root); + if (root[0] && root[strlen(root) - 1] == '/') + return cgit_fmtalloc("%s%s", root, ctx.qry.url); + return cgit_fmtalloc("%s/%s", root, ctx.qry.url); +} + +char *cgit_currentfullurl(void) +{ + const char *root = root_url(); + const char *orig_query = ctx.env.query_string ? ctx.env.query_string : ""; + size_t len = strlen(orig_query); + char *query = xmalloc(len + 2); + char *match, *ret; + + // The url argument carried the path into this request and the path is + // spelled out again below, so it is dropped here. The copy keeps a + // leading question mark so that every match has a character before it, + // which is what tells the argument itself from the tail of another one. + memcpy(query + 1, orig_query, len + 1); + query[0] = '?'; + match = query; + while ((match = strstr(match, "url=")) != NULL) { + if (match[-1] == '?' || match[-1] == '&') { + const char *next = strchr(match, '&'); + if (next) + memmove(match, next + 1, strlen(next)); + else + match[0] = '\0'; + } else + ++match; + } + if (!query[1]) + query[0] = '\0'; + + if (!ctx.qry.url) + ret = cgit_fmtalloc("%s%s", root, query); + else if (root[0] && root[strlen(root) - 1] == '/') + ret = cgit_fmtalloc("%s%s%s", root, ctx.qry.url, query); + else + ret = cgit_fmtalloc("%s/%s%s", root, ctx.qry.url, query); + free(query); + return ret; +} + +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; +} + +char *cgit_repourl(const char *reponame) +{ + if (ctx.cfg.virtual_root) + return cgit_fmtalloc("%s%s/", ctx.cfg.virtual_root, reponame); + else + return cgit_fmtalloc("?r=%s", reponame); +} + +char *cgit_fileurl(const char *reponame, const char *pagename, + const char *filename, const char *query) +{ + struct strbuf sb = STRBUF_INIT; + const char *delim; + + if (ctx.cfg.virtual_root) { + strbuf_addf(&sb, "%s%s/%s/%s", ctx.cfg.virtual_root, reponame, + pagename, (filename ? filename:"")); + delim = "?"; + } else { + strbuf_addf(&sb, "?url=%s/%s/%s", reponame, pagename, + (filename ? filename : "")); + delim = "&"; + } + if (query) + strbuf_addf(&sb, "%s%s", delim, query); + return strbuf_detach(&sb, NULL); +} + +char *cgit_pageurl(const char *reponame, const char *pagename, + const char *query) +{ + return cgit_fileurl(reponame, pagename, NULL, query); +} + +const char *cgit_snapshot_prefix(const struct cgit_repo *repo) +{ + if (repo->snapshot_prefix) + return repo->snapshot_prefix; + + return repo_basename(repo->url); +} + +void cgit_index_link(const char *name, const char *title, const char *class, + const char *pattern, const char *sort, int ofs, + int always_root) +{ + site_link(NULL, name, title, class, pattern, sort, ofs, always_root); +} + void cgit_summary_link(const char *name, const char *title, const char *class, const char *head) { @@ -368,12 +873,7 @@ void cgit_log_link(const char *name, const char *title, const char *class, const char *delim; delim = repolink(title, class, "log", head, path); - if (rev && ctx.qry.head && strcmp(rev, ctx.qry.head)) { - html(delim); - html("id="); - html_url_arg(rev); - delim = "&"; - } + delim = emit_rev_arg(delim, rev); if (grep && pattern) { html(delim); html("qt="); @@ -409,37 +909,13 @@ void cgit_commit_link(const char *name, const char *title, const char *class, const char *delim; delim = repolink(title, class, "commit", head, path); - if (rev && ctx.qry.head && strcmp(rev, ctx.qry.head)) { - html(delim); - html("id="); - html_url_arg(rev); - delim = "&"; - } - if (ctx.qry.difftype) { - html(delim); - htmlf("dt=%d", ctx.qry.difftype); - delim = "&"; - } - if (ctx.qry.context > 0 && ctx.qry.context != 3) { - html(delim); - html("context="); - htmlf("%d", ctx.qry.context); - delim = "&"; - } - if (ctx.qry.ignorews) { - html(delim); - html("ignorews=1"); - delim = "&"; - } - if (ctx.qry.follow) { - html(delim); - html("follow=1"); - } + delim = emit_rev_arg(delim, rev); + emit_diff_args(delim); html("'>"); if (name && name[0] != '\0') { - if (ctx.cfg.max_msg_len >= 15 && + 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 - 3); + html_ntxt(name, ctx.cfg.max_msg_len - ELLIPSIS_LEN); html("..."); } else html_txt(name); @@ -454,13 +930,6 @@ void cgit_refs_link(const char *name, const char *title, const char *class, reporevlink("refs", name, title, class, head, rev, path); } -void cgit_snapshot_link(const char *name, const char *title, const char *class, - const char *head, const char *rev, - const char *archivename) -{ - reporevlink("snapshot", name, title, class, head, rev, archivename); -} - void cgit_diff_link(const char *name, const char *title, const char *class, const char *head, const char *new_rev, const char *old_rev, const char *path) @@ -468,38 +937,14 @@ void cgit_diff_link(const char *name, const char *title, const char *class, const char *delim; delim = repolink(title, class, "diff", head, path); - if (new_rev && ctx.qry.head != NULL && strcmp(new_rev, ctx.qry.head)) { - html(delim); - html("id="); - html_url_arg(new_rev); - delim = "&"; - } + delim = emit_rev_arg(delim, new_rev); if (old_rev) { html(delim); html("id2="); html_url_arg(old_rev); delim = "&"; } - if (ctx.qry.difftype) { - html(delim); - htmlf("dt=%d", ctx.qry.difftype); - delim = "&"; - } - if (ctx.qry.context > 0 && ctx.qry.context != 3) { - html(delim); - html("context="); - htmlf("%d", ctx.qry.context); - delim = "&"; - } - if (ctx.qry.ignorews) { - html(delim); - html("ignorews=1"); - delim = "&"; - } - if (ctx.qry.follow) { - html(delim); - html("follow=1"); - } + emit_diff_args(delim); html("'>"); html_txt(name); html("</a>"); @@ -517,68 +962,6 @@ void cgit_stats_link(const char *name, const char *title, const char *class, reporevlink("stats", name, title, class, head, NULL, path); } -static void cgit_self_link(char *name, const char *title, const char *class) -{ - 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")) - cgit_summary_link(name, title, class, ctx.qry.head); - 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); - 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); - 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); - 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); - 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); - 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); - 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); - else if (!strcmp(ctx.qry.page, "snapshot")) - cgit_snapshot_link(name, title, class, ctx.qry.head, - ctx.qry.has_oid ? ctx.qry.oid : NULL, - 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 { - /* Don't known how to make link for this page */ - repolink(title, class, ctx.qry.page, ctx.qry.head, ctx.qry.path); - html("><!-- cgit_self_link() doesn't know how to make link for page '"); - html_txt(ctx.qry.page); - html("' -->"); - html_txt(name); - html("</a>"); - } -} - void cgit_object_link(struct object *obj) { const char *page; @@ -586,7 +969,7 @@ void cgit_object_link(struct object *obj) fullrev = oid_to_hex(&obj->oid); shortrev = xstrdup(fullrev); - shortrev[10] = '\0'; + shortrev[SHORT_HASH_LEN] = '\0'; if (obj->type == OBJ_COMMIT) { cgit_commit_link(cgit_fmt("commit %s...", shortrev), NULL, NULL, ctx.qry.head, fullrev, NULL); @@ -601,54 +984,6 @@ void cgit_object_link(struct object *obj) reporevlink(page, name, NULL, NULL, ctx.qry.head, fullrev, NULL); } -static struct string_list_item *lookup_path(struct string_list *list, - const char *path) -{ - struct string_list_item *item; - - while (path && path[0]) { - if ((item = string_list_lookup(list, path))) - return item; - if (!(path = strchr(path, '/'))) - break; - path++; - } - return NULL; -} - -/* - * Expand the %s placeholders in a module-link template and emit the result as - * an attribute value. The template is never handed to printf: a repository can - * supply its own through a repo-local cgitrc, and a surplus conversion would - * then read past the argument list. - */ -static void html_module_link(const char *tmpl, const char **args, int nargs) -{ - struct strbuf sb = STRBUF_INIT; - int used = 0; - - while (*tmpl) { - if (*tmpl != '%') { - strbuf_addch(&sb, *tmpl++); - continue; - } - tmpl++; - if (*tmpl == '%') { - strbuf_addch(&sb, '%'); - tmpl++; - } else if (*tmpl == 's' && used < nargs) { - strbuf_addstr(&sb, args[used++]); - tmpl++; - } else { - // Anything else is not a placeholder this template can - // fill, so keep it verbatim rather than dropping it. - strbuf_addch(&sb, '%'); - } - } - html_attr(sb.buf); - strbuf_release(&sb); -} - void cgit_submodule_link(const char *class, char *path, const char *rev) { struct string_list *list; @@ -674,9 +1009,8 @@ void cgit_submodule_link(const char *class, char *path, const char *rev) htmlf("class='%s' ", class); html("href='"); if (item) { - // A per-path template names only the submodule commit. const char *args[] = { rev }; - html_module_link(item->util, args, 1); + emit_module_link(item->util, args, 1); } else { const char *args[2]; dir = strrchr(path, '/'); @@ -686,7 +1020,7 @@ void cgit_submodule_link(const char *class, char *path, const char *rev) dir = path; args[0] = dir; args[1] = rev; - html_module_link(ctx.repo->module_link, args, 2); + emit_module_link(ctx.repo->module_link, args, 2); } html("'>"); html_txt(path); @@ -712,36 +1046,6 @@ struct date_mode cgit_date_mode(enum date_mode_type type) return mode; } -/* - * The mode date-format selects, used wherever an age column falls back to a - * calendar date. local-time still forces local rendering on top of it. - */ -static struct date_mode cgit_configured_date_mode(void) -{ - struct date_mode mode = ctx.cfg.date_mode; - - if (ctx.cfg.local_time) - mode.local = 1; - return mode; -} - -static void print_abs_date(time_t t, int tz) -{ - html("<span title='"); - html_attr(show_date(t, tz, cgit_date_mode(DATE_ISO8601))); - html("'>"); - html_txt(show_date(t, tz, cgit_configured_date_mode())); - html("</span>"); -} - -static void print_rel_date(time_t t, int tz, double value, - const char *class, const char *suffix) -{ - htmlf("<span class='%s' data-ut='%" PRIu64 "' title='", class, (uint64_t)t); - html_attr(show_date(t, tz, cgit_date_mode(DATE_ISO8601))); - htmlf("'>%.0f %s</span>", value, suffix); -} - void cgit_print_age(time_t t, int tz, time_t max_relative) { time_t now, secs; @@ -759,27 +1063,33 @@ void cgit_print_age(time_t t, int tz, time_t max_relative) return; } - if (secs < TM_HOUR * 2) { - print_rel_date(t, tz, secs * 1.0 / TM_MIN, "age-mins", "min."); + if (secs < SECONDS_PER_HOUR * 2) { + print_rel_date(t, tz, secs * 1.0 / SECONDS_PER_MINUTE, + "age-mins", "min."); return; } - if (secs < TM_DAY * 2) { - print_rel_date(t, tz, secs * 1.0 / TM_HOUR, "age-hours", "hours"); + if (secs < SECONDS_PER_DAY * 2) { + print_rel_date(t, tz, secs * 1.0 / SECONDS_PER_HOUR, + "age-hours", "hours"); return; } - if (secs < TM_WEEK * 2) { - print_rel_date(t, tz, secs * 1.0 / TM_DAY, "age-days", "days"); + if (secs < SECONDS_PER_WEEK * 2) { + print_rel_date(t, tz, secs * 1.0 / SECONDS_PER_DAY, + "age-days", "days"); return; } - if (secs < TM_MONTH * 2) { - print_rel_date(t, tz, secs * 1.0 / TM_WEEK, "age-weeks", "weeks"); + if (secs < SECONDS_PER_MONTH * 2) { + print_rel_date(t, tz, secs * 1.0 / SECONDS_PER_WEEK, + "age-weeks", "weeks"); return; } - if (secs < TM_YEAR * 2) { - print_rel_date(t, tz, secs * 1.0 / TM_MONTH, "age-months", "months"); + if (secs < SECONDS_PER_YEAR * 2) { + print_rel_date(t, tz, secs * 1.0 / SECONDS_PER_MONTH, + "age-months", "months"); return; } - print_rel_date(t, tz, secs * 1.0 / TM_YEAR, "age-years", "years"); + print_rel_date(t, tz, secs * 1.0 / SECONDS_PER_YEAR, + "age-years", "years"); } void cgit_print_http_headers(void) @@ -808,9 +1118,8 @@ void cgit_print_http_headers(void) if (ctx.page.etag) htmlf("ETag: \"%s\"\n", ctx.page.etag); html("\n"); - // Several pages follow the header block with output produced by git - // rather than by html_raw, the patch, raw diff and snapshot views among - // them. Emptying the buffer here keeps the headers ahead of it. + // Some pages follow the headers with output written by git itself, not + // through html_raw, so the buffer is emptied to keep the headers first. html_flush(); if (ctx.env.request_method && !strcmp(ctx.env.request_method, "HEAD")) exit(0); @@ -824,47 +1133,6 @@ void cgit_redirect(const char *url, bool permanent) html("\n\n"); } -static void print_rel_vcs_link(const char *url) -{ - html("<link rel='vcs-git' href='"); - html_attr(url); - html("' title='"); - html_attr(ctx.repo->name); - html(" Git repository'/>\n"); -} - -static int emit_css_link(struct string_list_item *s, void *arg) -{ - /* Do not emit anything if css= is specified. */ - if (s && *s->string == '\0') - return 0; - - html("<link rel='stylesheet' type='text/css' href='"); - if (s) - html_attr(s->string); - else - html_attr((const char *)arg); - html("'/>\n"); - - return 0; -} - -static int emit_js_link(struct string_list_item *s, void *arg) -{ - /* Do not emit anything if js= is specified. */ - if (s && *s->string == '\0') - return 0; - - html("<script type='text/javascript' src='"); - if (s) - html_attr(s->string); - else - html_attr((const char *)arg); - html("'></script>\n"); - - return 0; -} - void cgit_print_docstart(void) { char *host = cgit_hosturl(); @@ -875,7 +1143,7 @@ void cgit_print_docstart(void) return; } - html(cgit_doctype); + html("<!DOCTYPE html>\n"); html("<html lang='en' class='cgit-standalone'>\n"); html("<head>\n"); html("<meta charset='utf-8'/>\n"); @@ -962,13 +1230,14 @@ void cgit_print_error_page(int code, const char *msg, const char *fmt, ...) va_end(ap); } -void cgit_vprint_error_page(int code, const char *msg, const char *fmt, va_list ap) +void cgit_vprint_error_page(int code, const char *msg, const char *fmt, + va_list ap) { ctx.page.expires = ctx.cfg.cache_dynamic_ttl; ctx.page.status = code; ctx.page.statusmsg = msg; cgit_print_layout_start(); - cgit_vprint_error(fmt, ap); + vprint_error(fmt, ap); cgit_print_layout_end(); } @@ -984,23 +1253,6 @@ void cgit_print_layout_end(void) cgit_print_docend(); } -static void add_clone_urls(void (*fn)(const char *), char *txt, char *suffix) -{ - struct strbuf **url_list = strbuf_split_str(txt, ' ', 0); - int i; - - for (i = 0; url_list[i]; i++) { - strbuf_rtrim(url_list[i]); - if (url_list[i]->len == 0) - continue; - if (suffix && *suffix) - strbuf_addf(url_list[i], "/%s", suffix); - fn(url_list[i]->buf); - } - - strbuf_list_free(url_list); -} - void cgit_add_clone_urls(void (*fn)(const char *)) { if (ctx.repo->clone_url) @@ -1009,19 +1261,6 @@ void cgit_add_clone_urls(void (*fn)(const char *)) add_clone_urls(fn, ctx.cfg.clone_prefix, ctx.repo->url); } -static int print_branch_option(const struct reference *ref, void *cb_data) -{ - int *count = cb_data; - char *name = (char *)ref->name; - // The switcher runs on every page, so bound it like the refs list - // rather than emitting an option for every branch in the repo. - if (ctx.cfg.max_ref_count && *count >= ctx.cfg.max_ref_count) - return -1; - (*count)++; - html_option(name, name, ctx.qry.head); - return 0; -} - void cgit_add_hidden_formfields(int incl_head, int incl_search, const char *page) { @@ -1054,117 +1293,6 @@ void cgit_add_hidden_formfields(int incl_head, int incl_search, } } -static const char *hc(const char *page) -{ - if (!ctx.qry.page) - return NULL; - - return strcmp(ctx.qry.page, page) ? NULL : "active"; -} - -/* Build a context-sensitive tab title such as "Commit history of <path>". - * cgit_fmt() aborts the request rather than truncate, so fall back to the plain - * label when the path is long enough to risk overflowing its buffer. */ -static const char *tab_title(const char *format, const char *generic, - const char *vpath) -{ - if (!vpath || !*vpath || strlen(vpath) > 256) - return generic; - return cgit_fmt(format, vpath); -} - -static void cgit_print_path_crumbs(char *path) -{ - char *old_path = ctx.qry.path; - char *p = path, *q, *end = path + strlen(path); - int levels = 0; - - ctx.qry.path = NULL; - cgit_self_link("root", NULL, NULL); - ctx.qry.path = p = path; - while (p < end) { - if (!(q = strchr(p, '/')) || levels > 15) - q = end; - *q = '\0'; - html_txt("/"); - cgit_self_link(p, NULL, NULL); - if (q < end) - *q = '/'; - p = q + 1; - ++levels; - } - ctx.qry.path = old_path; -} - -static void print_header(void) -{ - const char *logo = NULL, *logo_link = NULL; - - if (ctx.repo && ctx.repo->logo && *ctx.repo->logo) - logo = ctx.repo->logo; - else - logo = ctx.cfg.logo; - if (ctx.repo && ctx.repo->logo_link && *ctx.repo->logo_link) - logo_link = ctx.repo->logo_link; - else - logo_link = ctx.cfg.logo_link; - - html("<header id='header'>\n"); - if (logo && *logo) { - html("<a class='logo' href='"); - if (logo_link && *logo_link) - html_attr(logo_link); - else - html_attr(cgit_rooturl()); - html("'><img src='"); - html_attr(logo); - html("' alt='cgit logo'/></a>\n"); - } - - html("<h1 class='main'>"); - if (ctx.repo) { - cgit_index_link("index", NULL, NULL, NULL, NULL, 0, 1); - html(" : "); - cgit_summary_link(ctx.repo->name, NULL, NULL, NULL); - } 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) { - html("<form class='branch-switch' method='get'>\n"); - cgit_add_hidden_formfields(0, 1, ctx.qry.page); - html("<select name='h' aria-label='Branch' data-autosubmit='1'>\n"); - int branch_count = 0; - refs_for_each_branch_ref(get_main_ref_store(the_repository), - print_branch_option, &branch_count); - if (ctx.repo->enable_remote_branches) - refs_for_each_remote_ref(get_main_ref_store(the_repository), - print_branch_option, &branch_count); - html("</select> "); - html("<input type='submit' value='switch'/>"); - html("</form>\n"); - } - - if (ctx.repo) { - html("<p class='sub desc'>"); - html_txt(ctx.repo->desc); - html("</p>\n"); - html("<p class='sub owner'>"); - html_txt(ctx.repo->owner); - html("</p>\n"); - } else if (ctx.cfg.root_desc) { - html("<p class='sub desc'>"); - html_txt(ctx.cfg.root_desc); - html("</p>\n"); - } - - html("<button type='button' class='theme-toggle' hidden " - "aria-label='Toggle color theme'></button>\n"); - html("</header>\n"); -} - void cgit_print_pageheader(void) { html("<div id='cgit'>"); @@ -1174,120 +1302,30 @@ void cgit_print_pageheader(void) if (ctx.env.authenticated) html("<nav class='tabs'>\n"); if (ctx.env.authenticated && ctx.repo && ctx.empty_repo) { - // Every other tab needs a commit to render, and the search form - // has nothing to search, so a repository with no commits gets - // summary alone rather than a row of links to the same notice. + // Every other tab needs a commit to render and the search form + // has nothing to search, so an empty repo gets summary alone. html("<ul>\n<li>"); - cgit_summary_link("summary", "Repository summary", hc("summary"), - ctx.qry.head); + cgit_summary_link("summary", "Repository summary", + tab_class("summary"), ctx.qry.head); html("</li>\n</ul>\n"); } else if (ctx.env.authenticated && ctx.repo) { - const char *vp = ctx.qry.vpath; - html("<ul>\n"); - if (ctx.repo->readme.nr) { - html("<li>"); - reporevlink("about", "about", "About this repository", - hc("about"), ctx.qry.head, NULL, - NULL); - html("</li>\n"); - } - html("<li>"); - cgit_summary_link("summary", "Repository summary", hc("summary"), - ctx.qry.head); - html("</li>\n<li>"); - cgit_refs_link("refs", "Branches and tags", hc("refs"), ctx.qry.head, - ctx.qry.oid, NULL); - html("</li>\n<li>"); - cgit_log_link("log", - tab_title("Commit history of %s", "Commit history", vp), - hc("log"), ctx.qry.head, - NULL, ctx.qry.vpath, 0, NULL, NULL, - ctx.qry.showmsg, ctx.qry.follow); - html("</li>\n<li>"); - if (ctx.qry.page && !strcmp(ctx.qry.page, "blame")) - cgit_blame_link("blame", - tab_title("Line-by-line blame of %s", "Blame", vp), - hc("blame"), ctx.qry.head, - ctx.qry.oid, ctx.qry.vpath); - else - cgit_tree_link("tree", - tab_title("Browse the tree at %s", "Browse the file tree", vp), - hc("tree"), ctx.qry.head, - ctx.qry.oid, ctx.qry.vpath); - html("</li>\n<li>"); - cgit_commit_link("commit", "The current commit", hc("commit"), - ctx.qry.head, ctx.qry.oid, ctx.qry.vpath); - html("</li>\n<li>"); - cgit_diff_link("diff", - tab_title("Changes to %s", "Changes in the current commit", vp), - hc("diff"), ctx.qry.head, - ctx.qry.oid, ctx.qry.oid2, ctx.qry.vpath); - html("</li>\n"); - if (ctx.repo->enable_stats) { - html("<li>"); - cgit_stats_link("stats", "Commit activity", hc("stats"), - ctx.qry.head, ctx.qry.vpath); - html("</li>\n"); - } - if (ctx.repo->homepage) { - html("<li><a class='external' href='"); - html_attr(ctx.repo->homepage); - html("'>homepage</a></li>\n"); - } - html("</ul>\n"); - html("<form class='search' method='get' action='"); - if (ctx.cfg.virtual_root) { - char *fileurl = cgit_fileurl(ctx.qry.repo, "log", - ctx.qry.vpath, NULL); - html_url_path(fileurl); - free(fileurl); - } - html("'>\n"); - cgit_add_hidden_formfields(1, 0, "log"); - html("<select name='qt' aria-label='Search type'>\n"); - html_option("grep", "log msg", ctx.qry.grep); - html_option("author", "author", ctx.qry.grep); - html_option("committer", "committer", ctx.qry.grep); - html_option("range", "range", ctx.qry.grep); - html("</select>\n"); - html("<input class='txt' type='search' name='q' aria-label='Search' value='"); - html_attr(ctx.qry.search); - html("'/>\n"); - html("<input type='submit' value='search'/>\n"); - html("</form>\n"); + print_repo_tabs(); + print_repo_search(); } else if (ctx.env.authenticated) { - char *currenturl = cgit_currenturl(); - html("<ul>\n<li>"); - site_link(NULL, "index", "Repository index", hc("repolist"), NULL, NULL, 0, 1); - html("</li>\n"); - if (ctx.cfg.root_readme) { - html("<li>"); - site_link("about", "about", "About this site", hc("about"), - NULL, NULL, 0, 1); - html("</li>\n"); - } - html("</ul>\n"); - html("<form class='search' method='get' action='"); - html_attr(currenturl); - html("'>\n"); - html("<input class='txt' type='search' name='q' aria-label='Search' value='"); - html_attr(ctx.qry.search); - html("'/>\n"); - html("<input type='submit' value='search'/>\n"); - html("</form>\n"); - free(currenturl); + print_site_tabs(); + print_site_search(); } if (ctx.env.authenticated) html("</nav>\n"); if (ctx.env.authenticated && ctx.repo && ctx.qry.vpath) { html("<nav class='path' aria-label='breadcrumb'>"); html("path: "); - cgit_print_path_crumbs(ctx.qry.vpath); + print_path_crumbs(ctx.qry.vpath); if (ctx.repo->enable_follow_links && !strcmp(ctx.qry.page, "log")) { html(" ("); ctx.qry.follow = !ctx.qry.follow; - cgit_self_link(ctx.qry.follow ? "follow" : "unfollow", - NULL, NULL); + self_link(ctx.qry.follow ? "follow" : "unfollow", + NULL, NULL); ctx.qry.follow = !ctx.qry.follow; html(")"); } @@ -1311,16 +1349,15 @@ void cgit_print_filemode(unsigned short mode) html_fileperm(mode); } -void cgit_compose_snapshot_prefix(struct strbuf *filename, const char *base, +static void compose_snapshot_prefix(struct strbuf *filename, const char *base, const char *ref) { struct object_id oid; - /* - * Prettify snapshot names by stripping leading "v" or "V" if the tag - * name starts with {v,V}[0-9] and the prettify mapping is injective, - * i.e. each stripped tag can be inverted without ambiguities. - */ + // 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 (repo_get_oid(the_repository, cgit_fmt("refs/tags/%s", ref), &oid) == 0 && (ref[0] == 'v' || ref[0] == 'V') && isdigit((unsigned char)ref[1]) && ((repo_get_oid(the_repository, cgit_fmt("refs/tags/%s", ref + 1), &oid) == 0) + @@ -1343,7 +1380,7 @@ void cgit_print_snapshot_links(const struct cgit_repo *repo, const char *ref, if (starts_with(ref, basename)) strbuf_addstr(&filename, ref); else - cgit_compose_snapshot_prefix(&filename, basename, ref); + compose_snapshot_prefix(&filename, basename, ref); prefixlen = filename.len; for (f = cgit_snapshot_formats; f->suffix; f++) { @@ -1351,19 +1388,23 @@ void cgit_print_snapshot_links(const struct cgit_repo *repo, const char *ref, continue; strbuf_setlen(&filename, prefixlen); strbuf_addstr(&filename, f->suffix); - cgit_snapshot_link(filename.buf, NULL, NULL, NULL, NULL, + snapshot_link(filename.buf, NULL, NULL, NULL, NULL, filename.buf); if (cgit_snapshot_get_sig(ref, f)) { strbuf_addstr(&filename, ".asc"); html(" ("); - cgit_snapshot_link("sig", NULL, NULL, NULL, NULL, + 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])) { - strbuf_setlen(&filename, strlen(filename.buf) - strlen(f->suffix)); + // A compressed tarball offers the signature made for + // the plain tar it expands to, which is the first + // format in the table. + strbuf_setlen(&filename, + filename.len - strlen(f->suffix)); strbuf_addstr(&filename, ".tar.asc"); html(" ("); - cgit_snapshot_link("sig", NULL, NULL, NULL, NULL, + snapshot_link("sig", NULL, NULL, NULL, NULL, filename.buf); html(")"); } @@ -1372,17 +1413,6 @@ void cgit_print_snapshot_links(const struct cgit_repo *repo, const char *ref, strbuf_release(&filename); } -/* Portable reverse memchr: memrchr is a glibc extension absent on - * macOS and the BSDs. */ -static const char *find_last_char(const char *start, const char *end, int c) -{ - while (end > start) { - if (*--end == c) - return end; - } - return NULL; -} - void cgit_set_title_from_path(const char *path) { struct strbuf sb = STRBUF_INIT; @@ -1391,9 +1421,14 @@ void cgit_set_title_from_path(const char *path) if (!path) return; - for (last_slash = path + strlen(path); (slash = find_last_char(path, last_slash, '/')) != NULL; last_slash = slash) { + // The path is read from the end so that the title names the file first + // and the directories it sits in after it. The separator between them + // is a guillemet, spelled here as the UTF-8 bytes of one. + last_slash = path + strlen(path); + while ((slash = find_last_char(path, last_slash, '/')) != NULL) { strbuf_add(&sb, slash + 1, last_slash - slash - 1); strbuf_addstr(&sb, " \xc2\xab "); + last_slash = slash; } strbuf_add(&sb, path, last_slash - path); strbuf_addf(&sb, " - %s", ctx.page.title); |
