/*
* 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 "html.h"
#include "shared.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
// Lines of context git itself defaults to, which a link has no reason to name.
#define DEFAULT_DIFF_CONTEXT 3
// Bounds the breadcrumbs, so that one request cannot turn into an unbounded
// row of links.
#define MAX_CRUMB_LEVELS 15
// 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("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';
}
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);
}
}
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("url);
if (ctx.repo->url[strlen(ctx.repo->url) - 1] != '/')
html("/");
if (page) {
html_url_arg(page);
html("/");
if (path)
html_url_arg(path);
}
delim = "&";
}
if (head && ctx.repo->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", ctx.qry.difftype);
delim = "&";
}
if (ctx.qry.context > 0 && ctx.qry.context != DEFAULT_DIFF_CONTEXT) {
html(delim);
html("context=");
htmlf("%d", ctx.qry.context);
delim = "&";
}
if (ctx.qry.ignorews) {
html(delim);
html("ignorews=1");
delim = "&";
}
if (ctx.qry.follow) {
html(delim);
html("follow=1");
}
}
static struct string_list_item *lookup_path(struct string_list *list,
const char *path)
{
struct string_list_item *item;
while (path && path[0]) {
if ((item = string_list_lookup(list, path)))
return item;
if (!(path = strchr(path, '/')))
break;
path++;
}
return NULL;
}
/*
* A repository can supply the module-link template through a repo-local
* cgitrc, so the %s placeholders are expanded here instead of the template
* being handed to printf, where a surplus conversion would read past the
* argument list.
*/
static void emit_module_link(const char *tmpl, const char **args, int nargs)
{
struct strbuf sb = STRBUF_INIT;
int used = 0;
while (*tmpl) {
if (*tmpl != '%') {
strbuf_addch(&sb, *tmpl++);
continue;
}
tmpl++;
if (*tmpl == '%') {
strbuf_addch(&sb, '%');
tmpl++;
} else if (*tmpl == 's' && used < nargs) {
strbuf_addstr(&sb, args[used++]);
tmpl++;
} else {
strbuf_addch(&sb, '%');
}
}
html_attr(sb.buf);
strbuf_release(&sb);
}
static struct date_mode configured_date_mode(void)
{
struct date_mode mode = ctx.cfg.date_mode;
if (ctx.cfg.local_time)
mode.local = 1;
return mode;
}
static void print_abs_date(time_t t, int tz)
{
html("");
html_txt(show_date(t, tz, configured_date_mode()));
html("");
}
static void print_rel_date(time_t t, int tz, double count, const char *class,
const char *suffix)
{
htmlf("%.0f %s", 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("\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);
}
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("\n");
if (logo && *logo) {
html("\n");
}
html("
\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("\n");
}
if (ctx.repo) {
html("
");
cgit_refs_link("refs", "Branches and tags", tab_class("refs"),
ctx.qry.head, ctx.qry.oid, NULL);
html("
\n
");
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("
\n
");
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("
\n
");
cgit_commit_link("commit", "The current commit", tab_class("commit"),
ctx.qry.head, ctx.qry.oid, ctx.qry.vpath);
html("
\n
");
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("
\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;
// 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) {
html_ntxt(name, ctx.cfg.max_msg_len - ELLIPSIS_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)
{
const char *page;
char *shortrev, *fullrev, *name;
fullrev = oid_to_hex(&obj->oid);
shortrev = xstrdup(fullrev);
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);
return;
} else if (obj->type == OBJ_TREE)
page = "tree";
else if (obj->type == OBJ_TAG)
page = "tag";
else
page = "blob";
name = cgit_fmt("%s %s...", type_name(obj->type), shortrev);
reporevlink(page, name, NULL, NULL, ctx.qry.head, fullrev, NULL);
}
void cgit_submodule_link(const char *class, char *path, const char *rev)
{
struct string_list *list;
struct string_list_item *item;
char tail, *dir;
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) {
html("util, args, 1);
} else {
const char *args[2];
dir = strrchr(path, '/');
if (dir)
dir++;
else
dir = path;
args[0] = dir;
args[1] = rev;
emit_module_link(ctx.repo->module_link, args, 2);
}
html("'>");
html_txt(path);
html("");
} else {
html("");
html_txt(path);
html("");
}
html_txtf(" @ %.7s", rev);
if (item && 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");
}
void cgit_print_http_headers(void)
{
if (ctx.env.no_http && !strcmp(ctx.env.no_http, "1"))
return;
if (ctx.page.status)
htmlf("Status: %d %s\n", ctx.page.status, ctx.page.statusmsg);
if (ctx.page.mimetype && ctx.page.charset)
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: %zd\n", ctx.page.size);
if (ctx.page.filename) {
html("Content-Disposition: inline; filename=\"");
html_header_arg_in_quotes(ctx.page.filename);
html("\"\n");
}
if (!ctx.env.authenticated)
html("Cache-Control: no-cache, no-store\n");
htmlf("Last-Modified: %s\n", http_date(ctx.page.modified));
htmlf("Expires: %s\n", http_date(ctx.page.expires));
if (ctx.page.etag)
htmlf("ETag: \"%s\"\n", ctx.page.etag);
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, bool permanent)
{
htmlf("Status: %d %s\n", permanent ? 301 : 302, permanent ? "Moved" : "Found");
html("Location: ");
html_url_path(url);
html("\n\n");
}
void cgit_print_docstart(void)
{
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);
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->extra_head_content)
html(ctx.repo->extra_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");
}
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)
{
ctx.page.expires = ctx.cfg.cache_dynamic_ttl;
ctx.page.status = code;
ctx.page.statusmsg = msg;
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);
}
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)
{
html("
");
if (!ctx.env.authenticated || !ctx.cfg.noheader)
print_header();
if (ctx.env.authenticated)
html("\n");
if (ctx.env.authenticated && ctx.repo && ctx.qry.vpath) {
html("");
}
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);
}
static void compose_snapshot_prefix(struct strbuf *filename, const char *base,
const char *ref)
{
struct object_id oid;
// 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) +
(repo_get_oid(the_repository, cgit_fmt("refs/tags/v%s", ref + 1), &oid) == 0) +
(repo_get_oid(the_repository, cgit_fmt("refs/tags/V%s", ref + 1), &oid) == 0) == 1))
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. 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);
ctx.page.title = strbuf_detach(&sb, NULL);
}