/* * 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 "cache.h" #include "cgit.h" #include "html.h" #include "shared.h" #include "submodule-config.h" #include "ui-shared.h" #include "ui-snapshot.h" #define DEFAULT_HTTP_PORT 80 #define SHORT_HASH_LEN 10 // 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 // Bounds the breadcrumbs, so that one request cannot turn into an unbounded // row of links. #define MAX_CRUMB_LEVELS 15 // 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 static const char *repo_basename(const char *reponame) { // 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; len = strlcpy(buf, reponame, sizeof(buf)); if (len >= sizeof(buf)) die("Repository name too long: %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'; } while (last && buf[last] == '/') buf[last--] = '\0'; slash = strrchr(buf, '/'); if (slash) return ++slash; return buf; } static const char *root_url(void) { if (ctx.cfg.virtual_root) return ctx.cfg.virtual_root; else return ctx.cfg.script_name; } 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(root_url()); else { char *currenturl = cgit_currenturl(); html_attr(currenturl); free(currenturl); } if (page) { htmlf("?p=%s", page); delim = "&"; } if (search) { html(delim); html("q="); html_url_arg(search); delim = "&"; } if (sort) { html(delim); html("s="); html_url_arg(sort); delim = "&"; } if (ofs) { html(delim); htmlf("ofs=%d", ofs); } } /* * Write the class of a link, and mark the link as the current one for * assistive technology when the class says it is the active tab or the * current page of a pager. */ static void link_class(const char *class) { if (!class) return; html(" class='"); html_attr(class); html("'"); if (!strcmp(class, "active") || !strcmp(class, "current")) html(" aria-current='page'"); } 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) { html(""); html_txt(name); html(""); } /* * 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 = "?"; html("defbranch && strcmp(head, ctx.repo->defbranch)) { html(delim); html("h="); html_url_arg(head); delim = "&"; } 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) { const char *delim; delim = repolink(title, class, page, head, path); emit_rev_arg(delim, rev); html("'>"); html_txt(name); html(""); } static void emit_diff_args(const char *delim) { if (ctx.qry.difftype) { html(delim); htmlf("dt=%d", (int)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; } /* * Dates render as time elements, whose datetime attribute wants the strict * ISO 8601 form with the T separator rather than the space git prints by * default. */ static void print_abs_date(time_t t, int tz) { html(""); } static void print_rel_date(time_t t, int tz, double count, const char *class, const char *suffix) { htmlf("", count, suffix); } /* * Whether a clone url opens with a URI scheme. The scp form git@host:path * has none, and a link element's href must hold a URI, so that form stays * out of the document head. */ static int has_uri_scheme(const char *url) { const char *p = url; if (!isalpha((unsigned char)*p)) return 0; while (isalnum((unsigned char)*p) || *p == '+' || *p == '-' || *p == '.') p++; return *p == ':'; } static void print_rel_vcs_link(const char *url) { if (!has_uri_scheme(url)) return; html("\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("\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("\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); } /* * The id parameter pins a page to one commit while the rest of the chrome * keeps naming the branch. Answer that commit, or null when the request * follows the branch or the pin lands on its tip anyway. */ static const struct object_id *pinned_oid(void) { static struct object_id oid; static int resolved, pinned; struct object_id head_oid; struct commit *commit; // The chrome asks several times per page and the answer cannot change // within a request. if (resolved) return pinned ? &oid : NULL; resolved = 1; if (!ctx.repo || !ctx.qry.has_oid || !ctx.qry.oid || !ctx.qry.head) return NULL; if (repo_get_oid(the_repository, ctx.qry.oid, &oid) || repo_get_oid(the_repository, ctx.qry.head, &head_oid)) return NULL; // A full hex id resolves without being looked up, so only peeling to a // commit proves the pin names history, and a tag pinned to the tip // compares equal only by its peeled commit. commit = lookup_commit_reference_gently(the_repository, &oid, 1); if (!commit) return NULL; oidcpy(&oid, &commit->object.oid); if (oideq(&oid, &head_oid)) return NULL; pinned = 1; return &oid; } struct branch_option_data { int count; const char *selected; }; /* * Whether the head the request follows is one of the branches the switcher * lists, remote ones included when those are shown. A head that is not, a * raw commit id or a tag, would otherwise leave the select resting on the * first branch, and submitting the form would quietly move the visitor there. */ static int head_is_listed_branch(void) { struct ref_store *refs = get_main_ref_store(the_repository); struct strbuf ref = STRBUF_INIT; int found; strbuf_addf(&ref, "refs/heads/%s", ctx.qry.head); found = refs_ref_exists(refs, ref.buf); if (!found && ctx.repo->enable_remote_branches) { strbuf_reset(&ref); strbuf_addf(&ref, "refs/remotes/%s", ctx.qry.head); found = refs_ref_exists(refs, ref.buf); } strbuf_release(&ref); return found; } static void print_detached_head_option(void) { const char *head = ctx.qry.head; struct object_id oid; struct strbuf label = STRBUF_INIT; // A head that is a full id reads better cut to the length the pinned // entry uses. if (strlen(head) == the_hash_algo->hexsz && !get_oid_hex(head, &oid)) strbuf_addf(&label, "%.*s (detached)", SHORT_HASH_LEN, head); else strbuf_addf(&label, "%s (detached)", head); html_option(head, label.buf, head); strbuf_release(&label); } static int print_branch_option(const struct reference *ref, void *data) { struct branch_option_data *opt = data; const char *name = ref->name; // The switcher runs on every page, so it is bounded like the refs list. if (ctx.cfg.max_ref_count && opt->count >= ctx.cfg.max_ref_count) return -1; // A name spelled like an option is refused as a head, so offering it // would only lead to an error page. if (name[0] == '-') return 0; opt->count++; html_option(name, name, opt->selected); 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("\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("\n"); } static void print_repo_search(void) { html("\n"); } static void print_site_tabs(void) { html("\n"); } static void print_site_search(void) { char *currenturl = cgit_currenturl(); html("\n"); free(currenturl); } 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); } /* * The link is built out of the request in ctx.qry, so a caller that alters a * field of ctx.qry first gets a link differing in exactly that. */ static void self_link(const char *name, const char *title, const char *class) { const char *rev = ctx.qry.has_oid ? ctx.qry.oid : NULL; if (!strcmp(ctx.qry.page, "repolist")) cgit_index_link(name, title, class, ctx.qry.search, ctx.qry.sort, ctx.qry.ofs, 1); else if (!strcmp(ctx.qry.page, "summary")) 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, rev, ctx.qry.path); else if (!strcmp(ctx.qry.page, "plain")) cgit_plain_link(name, title, class, ctx.qry.head, rev, ctx.qry.path); else if (!strcmp(ctx.qry.page, "blame")) cgit_blame_link(name, title, class, ctx.qry.head, rev, ctx.qry.path); else if (!strcmp(ctx.qry.page, "log")) cgit_log_link( name, title, class, ctx.qry.head, rev, ctx.qry.path, ctx.qry.ofs, ctx.qry.grep, ctx.qry.search, ctx.qry.showmsg, ctx.qry.follow ); else if (!strcmp(ctx.qry.page, "commit")) cgit_commit_link(name, title, class, ctx.qry.head, rev, ctx.qry.path); else if (!strcmp(ctx.qry.page, "patch")) cgit_patch_link(name, title, class, ctx.qry.head, rev, ctx.qry.path); else if (!strcmp(ctx.qry.page, "refs")) cgit_refs_link(name, title, class, ctx.qry.head, rev, ctx.qry.path); else if (!strcmp(ctx.qry.page, "snapshot")) snapshot_link(name, title, class, ctx.qry.head, rev, ctx.qry.path); else if (!strcmp(ctx.qry.page, "diff")) cgit_diff_link(name, title, class, ctx.qry.head, ctx.qry.oid, ctx.qry.oid2, ctx.qry.path); else if (!strcmp(ctx.qry.page, "stats")) cgit_stats_link(name, title, class, ctx.qry.head, ctx.qry.path); else { // A page name this switch does not know still gets a plain // repolink. repolink(title, class, ctx.qry.page, ctx.qry.head, ctx.qry.path); html("'>"); html_txt(name); html(""); } } 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("
"); va_copy(cp, ap); html_vtxtf(fmt, cp); va_end(cp); html("
\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_currentquery(void) { 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; // The url argument carried the path into this request and every caller // spells the path out again, 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="))) { 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'; return query; } char *cgit_currentfullurl(void) { const char *root = root_url(); char *query = cgit_currentquery(); char *ret; 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; // The result is a raw URL, so the query joiner is a bare ampersand // and each sink escapes the whole string for wherever it lands. if (ctx.cfg.virtual_root) { strbuf_addf(&sb, "%s%s/%s/%s", ctx.cfg.virtual_root, reponame, pagename, filename ? filename : ""); 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) { reporevlink(NULL, name, title, class, head, NULL, NULL); } void cgit_tag_link(const char *name, const char *title, const char *class, const char *tag) { reporevlink("tag", name, title, class, tag, NULL, NULL); } void cgit_tree_link(const char *name, const char *title, const char *class, const char *head, const char *rev, const char *path) { reporevlink("tree", name, title, class, head, rev, path); } void cgit_plain_link(const char *name, const char *title, const char *class, const char *head, const char *rev, const char *path) { reporevlink("plain", name, title, class, head, rev, path); } void cgit_blame_link(const char *name, const char *title, const char *class, const char *head, const char *rev, const char *path) { reporevlink("blame", name, title, class, head, rev, path); } void cgit_log_link(const char *name, const char *title, const char *class, const char *head, const char *rev, const char *path, int ofs, const char *grep, const char *pattern, int showmsg, int follow) { const char *delim; delim = repolink(title, class, "log", head, path); delim = emit_rev_arg(delim, rev); if (grep && pattern) { html(delim); html("qt="); html_url_arg(grep); delim = "&"; html(delim); html("q="); html_url_arg(pattern); } if (ofs > 0) { html(delim); html("ofs="); htmlf("%d", ofs); delim = "&"; } if (showmsg) { html(delim); html("showmsg=1"); delim = "&"; } if (follow) { html(delim); html("follow=1"); } html("'>"); html_txt(name); html(""); } void cgit_commit_link(const char *name, const char *title, const char *class, const char *head, const char *rev, const char *path) { const char *delim; delim = repolink(title, class, "commit", head, path); delim = emit_rev_arg(delim, rev); emit_diff_args(delim); html("'>"); if (name && name[0] != '\0') { if (ctx.cfg.max_msg_len >= MIN_TRUNCATE_LEN && strlen(name) > (size_t)ctx.cfg.max_msg_len) { size_t len = ctx.cfg.max_msg_len - ELLIPSIS_LEN; // A cut inside a multibyte character would leave an // invalid sequence on the page. while (len > 0 && (name[len] & 0xC0) == 0x80) len--; html_ntxt(name, len); html("..."); } else { html_txt(name); } } else { html_txt("(no commit message)"); } html(""); } void cgit_refs_link(const char *name, const char *title, const char *class, const char *head, const char *rev, const char *path) { reporevlink("refs", name, title, class, head, rev, path); } 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) { const char *delim; delim = repolink(title, class, "diff", head, path); delim = emit_rev_arg(delim, new_rev); if (old_rev) { html(delim); html("id2="); html_url_arg(old_rev); delim = "&"; } emit_diff_args(delim); html("'>"); html_txt(name); html(""); } void cgit_patch_link(const char *name, const char *title, const char *class, const char *head, const char *rev, const char *path) { reporevlink("patch", name, title, class, head, rev, path); } void cgit_stats_link(const char *name, const char *title, const char *class, const char *head, const char *path) { reporevlink("stats", name, title, class, head, NULL, path); } void cgit_object_link(struct object *obj, int full) { const char *page; char *shortrev, *fullrev, *name; fullrev = oid_to_hex(&obj->oid); if (full) { name = cgit_fmt("%s %s", type_name(obj->type), fullrev); } else { shortrev = xstrdup(fullrev); shortrev[SHORT_HASH_LEN] = '\0'; name = cgit_fmt("%s %s...", type_name(obj->type), shortrev); } if (obj->type == OBJ_COMMIT) { cgit_commit_link(name, NULL, NULL, ctx.qry.head, fullrev, NULL); return; } else if (obj->type == OBJ_TREE) page = "tree"; else if (obj->type == OBJ_TAG) page = "tag"; else page = "blob"; reporevlink(page, name, NULL, NULL, ctx.qry.head, fullrev, NULL); } /* * The tree being listed decides which .gitmodules applies, so the revision * the query names is resolved once and reused for every row. */ static const struct object_id *page_treeish(void) { static struct object_id oid; static int state; if (!state) { const char *rev = ctx.qry.oid ? ctx.qry.oid : ctx.qry.head; state = rev && !repo_get_oid(the_repository, rev, &oid) ? 1 : -1; } return state > 0 ? &oid : NULL; } static void strip_git_suffix(struct strbuf *sb) { while (sb->len && sb->buf[sb->len - 1] == '/') strbuf_setlen(sb, sb->len - 1); strbuf_strip_suffix(sb, ".git"); } /* * Reduces a submodule url to the path it names on its host, so the tail can * be compared against the urls this instance serves. A relative url points * at a sibling of the repository's own clone, which maps onto a sibling of * its cgit url. */ static char *submodule_url_path(const char *url) { struct strbuf sb = STRBUF_INIT; const char *rest, *colon; if (starts_with(url, "./") || starts_with(url, "../")) { strbuf_addf(&sb, "%s/%s", ctx.repo->url, url); if (strbuf_normalize_path(&sb)) { strbuf_release(&sb); return NULL; } } else if ( skip_prefix(url, "http://", &rest) || skip_prefix(url, "https://", &rest) || skip_prefix(url, "git://", &rest) || skip_prefix(url, "ssh://", &rest) ) { rest = strchr(rest, '/'); if (!rest) return NULL; strbuf_addstr(&sb, rest); } else if (skip_prefix(url, "file://", &rest)) { strbuf_addstr(&sb, rest); } else if ((colon = strchr(url, ':')) && !memchr(url, '/', colon - url)) { // scp syntax, user@host:path strbuf_addstr(&sb, colon + 1); } else { strbuf_addstr(&sb, url); } strip_git_suffix(&sb); while (sb.len && sb.buf[0] == '/') strbuf_remove(&sb, 0, 1); if (!sb.len) { strbuf_release(&sb); return NULL; } return strbuf_detach(&sb, NULL); } /* * Finds the served repository whose url matches the most trailing components * of the submodule url, whatever scheme carried it. */ static struct cgit_repo *repo_serving_url(const char *url) { struct cgit_repo *repo, *best = NULL; char *path = submodule_url_path(url); size_t plen, rlen, blen = 0; int i; if (!path) return NULL; plen = strlen(path); for (i = 0; i < cgit_repolist.count; i++) { repo = &cgit_repolist.repos[i]; if (repo->ignore) continue; rlen = strlen(repo->url); if (!rlen || rlen > plen) continue; if (strcmp(path + plen - rlen, repo->url)) continue; // Only whole trailing components count, so x/foo cannot // claim a repository named oo. if (rlen < plen && path[plen - rlen - 1] != '/') continue; if (rlen > blen) { best = repo; blen = rlen; } } free(path); return best; } // Hosts whose repository and commit pages follow a known form, so an ssh url // can still be offered as a browser link. kernel.org runs cgit, which keeps // the .git suffix in its own page urls. static const struct forge { const char *host; const char *commit_seg; int keep_dot_git; } forges[] = { { "github.com", "/commit/", 0 }, { "gitlab.com", "/-/commit/", 0 }, { "bitbucket.org", "/commits/", 0 }, { "codeberg.org", "/commit/", 0 }, { "gitea.com", "/commit/", 0 }, { "git.sr.ht", "/commit/", 0 }, { "git.kernel.org", "/commit/?id=", 1 }, }; static const struct forge *forge_for_host(const char *host, size_t len) { size_t i; for (i = 0; i < ARRAY_SIZE(forges); i++) if (strlen(forges[i].host) == len && !strncasecmp(forges[i].host, host, len)) return &forges[i]; return NULL; } /* * Derives a submodule row's links from its .gitmodules entry. The url is * matched against this instance's own repositories first, so ssh, file and * relative urls still land on an internal page when their target is served * here, and a scheme cgit cannot vouch for is left unlinked as a tooltip. */ static void gitmodules_link(const char *path, const char *rev, char **module, char **commit, const char **tooltip) { const struct object_id *treeish = page_treeish(); const struct submodule *sub; const struct forge *forge; struct cgit_repo *target; struct strbuf sb = STRBUF_INIT; const char *url, *rest, *colon; if (!treeish) return; sub = submodule_from_path(the_repository, treeish, path); if (!sub || !sub->url) return; url = sub->url; target = repo_serving_url(url); if (target) { char *query = cgit_fmtalloc("id=%s", rev); *module = cgit_pageurl(target->url, "tree", query); *commit = cgit_pageurl(target->url, "commit", query); free(query); return; } if (skip_prefix(url, "http://", &rest) || skip_prefix(url, "https://", &rest)) { *module = xstrdup(url); forge = forge_for_host(rest, strcspn(rest, "/")); if (forge) { strbuf_addstr(&sb, url); while (sb.len && sb.buf[sb.len - 1] == '/') strbuf_setlen(&sb, sb.len - 1); if (!forge->keep_dot_git) strbuf_strip_suffix(&sb, ".git"); strbuf_addstr(&sb, forge->commit_seg); strbuf_addstr(&sb, rev); *commit = strbuf_detach(&sb, NULL); } return; } if ((colon = strchr(url, ':')) && !memchr(url, '/', colon - url)) { const char *host = url; const char *at = memchr(url, '@', colon - url); if (at) host = at + 1; forge = forge_for_host(host, colon - host); if (forge) { rest = colon + 1; while (*rest == '/') rest++; strbuf_addf(&sb, "https://%.*s/%s", (int)(colon - host), host, rest); while (sb.len && sb.buf[sb.len - 1] == '/') strbuf_setlen(&sb, sb.len - 1); if (!forge->keep_dot_git) strbuf_strip_suffix(&sb, ".git"); *module = xstrdup(sb.buf); strbuf_addstr(&sb, forge->commit_seg); strbuf_addstr(&sb, rev); *commit = strbuf_detach(&sb, NULL); return; } } *tooltip = url; } void cgit_submodule_link(const char *class, char *path, const char *rev) { struct string_list *list; struct string_list_item *item; char tail, *module = NULL, *commit = NULL; const char *name, *tooltip = NULL; size_t len; len = 0; tail = 0; list = &ctx.repo->submodules; item = lookup_path(list, path); if (!item) { len = strlen(path); tail = path[len - 1]; if (tail == '/') { path[len - 1] = 0; item = lookup_path(list, path); } } if (!item && !ctx.repo->module_link && ctx.repo->enable_gitmodules_links) gitmodules_link(path, rev, &module, &commit, &tooltip); // The row shows the entry's own name, like every other row, while // the lookups above want the full path. name = strrchr(path, '/'); name = name ? name + 1 : path; if (item || ctx.repo->module_link || module) { html(""); html_txt(name); html(""); } else { html(""); html_txt(name); html(""); } html(" @ "); if (commit) { html(""); html_txtf("%.7s", rev); html(commit ? "" : ""); free(module); free(commit); if (tail == '/') path[len - 1] = tail; } struct date_mode cgit_date_mode(enum date_mode_type type) { static struct date_mode mode; mode.type = type; mode.local = ctx.cfg.local_time; return mode; } void cgit_print_age(time_t t, int tz, time_t max_relative) { time_t now, secs; if (!t) return; time(&now); secs = now - t; if (secs < 0) secs = 0; if (!ctx.cfg.enable_relative_dates || (secs > max_relative && max_relative >= 0)) { print_abs_date(t, tz); return; } if (secs < SECONDS_PER_HOUR * 2) { print_rel_date(t, tz, secs * 1.0 / SECONDS_PER_MINUTE, "age-mins", "min."); return; } if (secs < SECONDS_PER_DAY * 2) { print_rel_date(t, tz, secs * 1.0 / SECONDS_PER_HOUR, "age-hours", "hours"); return; } if (secs < SECONDS_PER_WEEK * 2) { print_rel_date(t, tz, secs * 1.0 / SECONDS_PER_DAY, "age-days", "days"); return; } if (secs < SECONDS_PER_MONTH * 2) { print_rel_date(t, tz, secs * 1.0 / SECONDS_PER_WEEK, "age-weeks", "weeks"); return; } 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 / SECONDS_PER_YEAR, "age-years", "years"); } /* * Whether a content type carries text a charset applies to, which is every * text type and the XML ones, the atom feed among them. */ static int type_has_charset(const char *mimetype) { return starts_with(mimetype, "text/") || strstr(mimetype, "xml"); } void cgit_print_http_headers(void) { ctx.page.headers_sent = 1; if (ctx.env.no_http && !strcmp(ctx.env.no_http, "1")) return; // CGI treats a missing Status as 200, but writing it keeps every // response's header block the same shape. if (ctx.page.status) htmlf("Status: %d %s\n", ctx.page.status, ctx.page.statusmsg); else html("Status: 200 OK\n"); // A charset only means something for text, so an archive or a pack // is sent without one. if (ctx.page.mimetype && ctx.page.charset && type_has_charset(ctx.page.mimetype)) htmlf("Content-Type: %s; charset=%s\n", ctx.page.mimetype, ctx.page.charset); else if (ctx.page.mimetype) htmlf("Content-Type: %s\n", ctx.page.mimetype); if (ctx.page.size) htmlf("Content-Length: %zu\n", ctx.page.size); if (ctx.page.filename) { html("Content-Disposition: inline; filename=\""); html_header_arg_in_quotes(ctx.page.filename); html("\"\n"); } // An untrusted page serves repository bytes verbatim, so the browser is // told not to guess a type of its own from them and not to load // anything they reference. if (ctx.page.untrusted) { html("X-Content-Type-Options: nosniff\n"); html("Content-Security-Policy: default-src 'none'\n"); } // A page behind an auth filter is for the visitor who was let in, so a // cache shared with other visitors must not keep it, and it varies on // the cookie that got them in. if (!ctx.env.authenticated) html("Cache-Control: no-cache, no-store\n"); else if (ctx.cfg.auth_filter) html("Cache-Control: private\nVary: Cookie\n"); html("\n"); // 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); } void cgit_redirect(const char *url, const char *query, bool permanent) { htmlf("Status: %d %s\n", permanent ? 301 : 302, permanent ? "Moved Permanently" : "Found"); html("Location: "); html_url_path(url); if (query) html_header_query(query); html("\n\n"); } void cgit_print_docstart(void) { const struct object_id *pinned; char *host = cgit_hosturl(); if (ctx.cfg.embedded) { if (ctx.cfg.header) html_include(ctx.cfg.header); return; } html("\n"); html("\n"); html("\n"); html("\n"); html("\n"); html("\n"); html("\n"); html("\n"); html(""); // An error page reached before a title was chosen still has to name // itself, since an empty title element is not valid. html_txt(ctx.page.title ? ctx.page.title : ctx.cfg.root_title); // The auth page goes out before the repository is opened, when // pinned_oid cannot resolve anything yet. if (ctx.env.authenticated && (pinned = pinned_oid())) html_txtf(" @ %.*s", SHORT_HASH_LEN, oid_to_hex(pinned)); html("\n"); // The placeholder a scanned repository carries instead of a description // is left out, since it describes nothing. if (ctx.repo && ctx.repo->desc && *ctx.repo->desc && ctx.repo->desc != cgit_default_repo_desc) { html("\n"); } else if (!ctx.repo && ctx.cfg.root_desc && *ctx.cfg.root_desc) { html("\n"); } htmlf("\n", cgit_version); if (ctx.cfg.robots && *ctx.cfg.robots) { html("\n"); } if (ctx.cfg.css.items) for_each_string_list(&ctx.cfg.css, emit_css_link, NULL); else emit_css_link(NULL, "/cgit.css"); if (ctx.cfg.js.items) for_each_string_list(&ctx.cfg.js, emit_js_link, NULL); else emit_js_link(NULL, "/cgit.js"); if (ctx.cfg.favicon && *ctx.cfg.favicon) { html("\n"); } if (host && ctx.repo && ctx.qry.head) { char *fileurl; struct strbuf sb = STRBUF_INIT; strbuf_addf(&sb, "h=%s", ctx.qry.head); html("\n"); strbuf_release(&sb); free(fileurl); } if (ctx.repo) cgit_add_clone_urls(print_rel_vcs_link); if (ctx.cfg.head_include) html_include(ctx.cfg.head_include); if (ctx.repo && ctx.repo->head_content) html(ctx.repo->head_content); html("\n"); html("\n"); if (ctx.cfg.header) html_include(ctx.cfg.header); free(host); } void cgit_print_docend(void) { html("\n"); if (ctx.cfg.embedded) { html("\n"); if (ctx.cfg.footer) html_include(ctx.cfg.footer); return; } if (ctx.cfg.footer) html_include(ctx.cfg.footer); else { htmlf("\n", cgit_version, git_version_string); } html("\n"); html("\n\n"); } void cgit_print_error_page(int code, const char *msg, const char *fmt, ...) { va_list ap; va_start(ap, fmt); cgit_vprint_error_page(code, msg, fmt, ap); va_end(ap); } void cgit_vprint_error_page(int code, const char *msg, const char *fmt, va_list ap) { // An error page describes a condition that may not outlast the slot // holding it. A request naming a commit not yet pushed would cache its // 404 under the never-expiring static ttl and keep serving it after // the push, so the error is rendered straight to the visitor instead. html_flush(); cache_abandon_fill(); // Once the status line is out the error can only join the body under // way, and the page that hit it carries on to its own end. if (ctx.page.headers_sent) { vprint_error(fmt, ap); return; } ctx.page.status = code; ctx.page.statusmsg = msg; // A page that had already chosen another type, such as an image // served through the about page, still answers its error as html. ctx.page.mimetype = "text/html"; ctx.page.charset = PAGE_ENCODING; ctx.page.filename = NULL; cgit_print_layout_start(); vprint_error(fmt, ap); cgit_print_layout_end(); } void cgit_print_layout_start(void) { cgit_print_http_headers(); cgit_print_docstart(); cgit_print_pageheader(); } void cgit_print_layout_end(void) { cgit_print_docend(); } void cgit_add_clone_urls(void (*fn)(const char *)) { if (ctx.repo->clone_url) add_clone_urls(fn, cgit_expand_macros(ctx.repo->clone_url), NULL); else if (ctx.cfg.clone_prefix) add_clone_urls(fn, ctx.cfg.clone_prefix, ctx.repo->url); } static char *http_clone_url; static int http_clone_url_scanned; /* * Scheme names compare case-insensitively. */ static int is_http_url(const char *url) { return istarts_with(url, "http://") || istarts_with(url, "https://"); } static void capture_http_clone_url(const char *url) { if (!http_clone_url && is_http_url(url)) http_clone_url = xstrdup(url); } /* * One row of the clone table. Every row is a link, but a browser cannot * follow the ssh, scp or git forms, so those rows point at the first http * url in the clone list, or at the repository's own page when the list has * none. A colspan of zero leaves the attribute out. */ void cgit_print_clone_row(const char *url, int colspan) { const char *href = url; char *fallback = NULL; if (!is_http_url(url)) { if (!http_clone_url_scanned) { http_clone_url_scanned = 1; cgit_add_clone_urls(capture_http_clone_url); } href = http_clone_url; if (!href) href = fallback = cgit_repourl(ctx.repo->url); } if (colspan) htmlf("", colspan); else html(""); html(""); html_txt(url); html("\n"); free(fallback); } void cgit_add_hidden_formfields(int incl_head, int incl_search, const char *page) { if (!ctx.cfg.virtual_root) { struct strbuf url = STRBUF_INIT; strbuf_addf(&url, "%s/%s", ctx.qry.repo, page); if (ctx.qry.vpath) strbuf_addf(&url, "/%s", ctx.qry.vpath); html_hidden("url", url.buf); strbuf_release(&url); } if (incl_head && ctx.qry.head && ctx.repo->defbranch && strcmp(ctx.qry.head, ctx.repo->defbranch)) html_hidden("h", ctx.qry.head); if (ctx.qry.oid) html_hidden("id", ctx.qry.oid); if (ctx.qry.oid2) html_hidden("id2", ctx.qry.oid2); if (ctx.qry.showmsg) html_hidden("showmsg", "1"); if (incl_search) { if (ctx.qry.grep) html_hidden("qt", ctx.qry.grep); if (ctx.qry.search) html_hidden("q", ctx.qry.search); } } void cgit_print_pageheader(void) { const struct object_id *pinned; // The root names the page and the repository so a stylesheet or script // can tell them apart without reading the tabs. html("
\n"); if (!ctx.env.authenticated || ctx.cfg.enable_header) print_header(); if (ctx.env.authenticated) html("\n"); pinned = ctx.env.authenticated && ctx.repo ? pinned_oid() : NULL; // The commit and diff pages name the commit they show as their own // content, so the rev crumb would only repeat them. if (pinned && ctx.qry.page && (!strcmp(ctx.qry.page, "commit") || !strcmp(ctx.qry.page, "diff"))) pinned = NULL; if ((ctx.env.authenticated && ctx.repo && ctx.qry.vpath) || pinned) { html("\n"); } html("
\n"); } void cgit_print_filemode(unsigned short mode) { if (S_ISDIR(mode)) html("d"); else if (S_ISLNK(mode)) html("l"); else if (S_ISGITLINK(mode)) html("m"); else html("-"); html_fileperm(mode >> 6); html_fileperm(mode >> 3); html_fileperm(mode); } // The tag names and their stems, a stem being the name with any leading v or V // removed, gathered once per request. Claims on a stem are counted over the // tag list itself rather than by looking refs up by name, because a loose ref // lookup on a case-insensitive filesystem finds v1.2 when asked for V1.2 and // would call every tag still stored loose ambiguous. static struct string_list tag_names = STRING_LIST_INIT_DUP; static struct string_list tag_stems = STRING_LIST_INIT_DUP; static int tags_collected; static int collect_tag(const struct reference *ref, void *data) { const char *name = ref->name; string_list_append(&tag_names, name); if (name[0] == 'v' || name[0] == 'V') name++; string_list_append(&tag_stems, name); return 0; } /* * Whether ref is a tag whose stem no other tag shares. */ static int stem_is_unique(const char *ref) { struct string_list_item *stem; size_t i; if (!tags_collected) { tags_collected = 1; refs_for_each_tag_ref(get_main_ref_store(the_repository), collect_tag, NULL); string_list_sort(&tag_names); string_list_sort(&tag_stems); } if (!string_list_has_string(&tag_names, ref)) return 0; stem = string_list_lookup(&tag_stems, ref + 1); if (!stem) return 0; // The lookup lands on any one of equal entries, so both neighbours // are checked. i = stem - tag_stems.items; return (i == 0 || strcmp(tag_stems.items[i - 1].string, stem->string)) && (i + 1 == tag_stems.nr || strcmp(tag_stems.items[i + 1].string, stem->string)); } static void compose_snapshot_prefix(struct strbuf *filename, const char *base, const char *ref) { // A tag named v1.2 or V1.2 gives its snapshot the prettier name 1.2, // but only where dropping the letter cannot land two different tags on // one name, so that a snapshot can still be traced back to the tag it // was made from. if ((ref[0] == 'v' || ref[0] == 'V') && isdigit((unsigned char)ref[1]) && stem_is_unique(ref)) ref++; strbuf_addf(filename, "%s-%s", base, ref); } void cgit_print_snapshot_links(const struct cgit_repo *repo, const char *ref, const char *separator) { const struct cgit_snapshot_format *f; struct strbuf filename = STRBUF_INIT; const char *basename; size_t prefixlen; basename = cgit_snapshot_prefix(repo); if (starts_with(ref, basename)) strbuf_addstr(&filename, ref); else compose_snapshot_prefix(&filename, basename, ref); prefixlen = filename.len; for (f = cgit_snapshot_formats; f->suffix; f++) { if (!(repo->snapshots & cgit_snapshot_format_bit(f))) continue; strbuf_setlen(&filename, prefixlen); strbuf_addstr(&filename, f->suffix); snapshot_link(filename.buf, NULL, NULL, NULL, NULL, filename.buf); if (cgit_snapshot_get_sig(ref, f)) { strbuf_addstr(&filename, ".asc"); html(" ("); snapshot_link("sig", NULL, NULL, NULL, NULL, filename.buf); html(")"); } else if ( starts_with(f->suffix, ".tar") && cgit_snapshot_get_sig(ref, &cgit_snapshot_formats[0]) ) { // 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(" ("); snapshot_link("sig", NULL, NULL, NULL, NULL, filename.buf); html(")"); } html(separator); } strbuf_release(&filename); } void cgit_set_title_from_path(const char *path) { struct strbuf sb = STRBUF_INIT; const char *slash, *last_slash; if (!path) return; // The path is read from the end so that the title names the file first // and the directories it sits in after it. last_slash = path + strlen(path); while ((slash = find_last_char(path, last_slash, '/'))) { strbuf_add(&sb, slash + 1, last_slash - slash - 1); strbuf_addstr(&sb, " < "); last_slash = slash; } strbuf_add(&sb, path, last_slash - path); strbuf_addf(&sb, " - %s", ctx.page.title); ctx.page.title = strbuf_detach(&sb, NULL); }