diff options
context:
space:
mode:
Diffstat (limited to 'source/ui-shared.c')
-rw-r--r--source/ui-shared.c1491
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 = "&amp;";
- }
- 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 "&amp;";
+ }
+ 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 = "&amp;";
+ }
+ if (ctx.qry.context > 0 && ctx.qry.context != DEFAULT_DIFF_CONTEXT) {
+ html(delim);
+ html("context=");
+ htmlf("%d", ctx.qry.context);
+ delim = "&amp;";
+ }
+ if (ctx.qry.ignorews) {
+ html(delim);
+ html("ignorews=1");
+ delim = "&amp;";
+ }
+ 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 = "&amp;";
+ }
+ 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 = "&amp;";
- }
+ 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 = "&amp;";
- }
- if (ctx.qry.difftype) {
- html(delim);
- htmlf("dt=%d", ctx.qry.difftype);
- delim = "&amp;";
- }
- if (ctx.qry.context > 0 && ctx.qry.context != 3) {
- html(delim);
- html("context=");
- htmlf("%d", ctx.qry.context);
- delim = "&amp;";
- }
- if (ctx.qry.ignorews) {
- html(delim);
- html("ignorews=1");
- delim = "&amp;";
- }
- 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 = "&amp;";
- }
+ delim = emit_rev_arg(delim, new_rev);
if (old_rev) {
html(delim);
html("id2=");
html_url_arg(old_rev);
delim = "&amp;";
}
- if (ctx.qry.difftype) {
- html(delim);
- htmlf("dt=%d", ctx.qry.difftype);
- delim = "&amp;";
- }
- if (ctx.qry.context > 0 && ctx.qry.context != 3) {
- html(delim);
- html("context=");
- htmlf("%d", ctx.qry.context);
- delim = "&amp;";
- }
- if (ctx.qry.ignorews) {
- html(delim);
- html("ignorews=1");
- delim = "&amp;";
- }
- 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);