40803d9c6d
cgit-pink is a maintained fork of cgit that follows upstream git more closely and already contains a lot of patches we already had applied. Consequently, it seems sensible it becomes our future upstream, we may even be able to upstream some of our custom, less invasive patches. Change-Id: Ia081e4508866f32298986c7160f4890c8a7c8922
1239 lines
31 KiB
C
1239 lines
31 KiB
C
/* 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 COPYING for full license text)
|
|
*/
|
|
|
|
#include "cgit.h"
|
|
#include "ui-shared.h"
|
|
#include "cmd.h"
|
|
#include "html.h"
|
|
#include "version.h"
|
|
|
|
static const char cgit_doctype[] =
|
|
"<!DOCTYPE html>\n";
|
|
|
|
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 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);
|
|
}
|
|
|
|
void cgit_print_error(const char *fmt, ...)
|
|
{
|
|
va_list ap;
|
|
va_start(ap, fmt);
|
|
cgit_vprint_error(fmt, ap);
|
|
va_end(ap);
|
|
}
|
|
|
|
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");
|
|
}
|
|
|
|
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 fmtalloc("%s:%s", ctx.env.server_name, ctx.env.server_port);
|
|
}
|
|
|
|
char *cgit_currenturl(void)
|
|
{
|
|
const char *root = cgit_rooturl();
|
|
|
|
if (!ctx.qry.url)
|
|
return xstrdup(root);
|
|
if (root[0] && root[strlen(root) - 1] == '/')
|
|
return fmtalloc("%s%s", root, ctx.qry.url);
|
|
return fmtalloc("%s/%s", root, ctx.qry.url);
|
|
}
|
|
|
|
char *cgit_currentfullurl(void)
|
|
{
|
|
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;
|
|
|
|
/* 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;
|
|
}
|
|
if (!query[1])
|
|
query[0] = '\0';
|
|
|
|
if (!ctx.qry.url)
|
|
ret = fmtalloc("%s%s", root, query);
|
|
else if (root[0] && root[strlen(root) - 1] == '/')
|
|
ret = fmtalloc("%s%s%s", root, ctx.qry.url, query);
|
|
else
|
|
ret = fmtalloc("%s/%s%s", root, ctx.qry.url, query);
|
|
free(query);
|
|
return ret;
|
|
}
|
|
|
|
const char *cgit_rooturl(void)
|
|
{
|
|
if (ctx.cfg.virtual_root)
|
|
return ctx.cfg.virtual_root;
|
|
else
|
|
return ctx.cfg.script_name;
|
|
}
|
|
|
|
const char *cgit_loginurl(void)
|
|
{
|
|
static const char *login_url;
|
|
if (!login_url)
|
|
login_url = fmtalloc("%s?p=login", cgit_rooturl());
|
|
return login_url;
|
|
}
|
|
|
|
char *cgit_repourl(const char *reponame)
|
|
{
|
|
// my cgit instance *only* serves the depot, hence that's the only value ever
|
|
// needed.
|
|
return fmtalloc("/");
|
|
}
|
|
|
|
char *cgit_fileurl(const char *reponame, const char *pagename,
|
|
const char *filename, const char *query)
|
|
{
|
|
struct strbuf sb = STRBUF_INIT;
|
|
|
|
strbuf_addf(&sb, "%s%s/%s", ctx.cfg.virtual_root,
|
|
pagename, (filename ? filename:""));
|
|
|
|
if (query) {
|
|
strbuf_addf(&sb, "%s%s", "?", 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)
|
|
{
|
|
char *delim = "?";
|
|
|
|
if (always_root || page)
|
|
html_attr(cgit_rooturl());
|
|
else {
|
|
char *currenturl = cgit_currenturl();
|
|
html_attr(currenturl);
|
|
free(currenturl);
|
|
}
|
|
|
|
if (page) {
|
|
htmlf("?p=%s", page);
|
|
delim = "&";
|
|
}
|
|
if (search) {
|
|
html(delim);
|
|
html("q=");
|
|
html_attr(search);
|
|
delim = "&";
|
|
}
|
|
if (sort) {
|
|
html(delim);
|
|
html("s=");
|
|
html_attr(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("<a");
|
|
if (title) {
|
|
html(" title='");
|
|
html_attr(title);
|
|
html("'");
|
|
}
|
|
if (class) {
|
|
html(" class='");
|
|
html_attr(class);
|
|
html("'");
|
|
}
|
|
html(" href='");
|
|
site_url(page, search, sort, ofs, always_root);
|
|
html("'>");
|
|
html_txt(name);
|
|
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 char *repolink(const char *title, const char *class, const char *page,
|
|
const char *head, const char *path)
|
|
{
|
|
char *delim = "?";
|
|
|
|
html("<a");
|
|
if (title) {
|
|
html(" title='");
|
|
html_attr(title);
|
|
html("'");
|
|
}
|
|
if (class) {
|
|
html(" class='");
|
|
html_attr(class);
|
|
html("'");
|
|
}
|
|
html(" href='");
|
|
if (ctx.cfg.virtual_root) {
|
|
html_url_path(ctx.cfg.virtual_root);
|
|
if (page) {
|
|
html_url_path(page);
|
|
html("/");
|
|
if (path)
|
|
html_url_path(path);
|
|
}
|
|
} else {
|
|
html_url_path(ctx.cfg.script_name);
|
|
html("?url=");
|
|
html_url_arg(ctx.repo->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 fmt("%s", 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)
|
|
{
|
|
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);
|
|
}
|
|
html("'>");
|
|
html_txt(name);
|
|
html("</a>");
|
|
}
|
|
|
|
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_about_link(const char *name, const char *title, const char *class,
|
|
const char *head, const char *rev, const char *path)
|
|
{
|
|
reporevlink("about", name, title, class, head, rev, path);
|
|
}
|
|
|
|
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)
|
|
{
|
|
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 = "&";
|
|
}
|
|
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("</a>");
|
|
}
|
|
|
|
void cgit_commit_link(const char *name, const char *title, const char *class,
|
|
const char *head, const char *rev, const char *path)
|
|
{
|
|
char *delim;
|
|
|
|
delim = repolink(title, class, "commit", head, path);
|
|
if (rev && ctx.qry.head && strcmp(rev, ctx.qry.head)) {
|
|
html(delim);
|
|
html("id=");
|
|
html_url_arg(rev);
|
|
delim = "&";
|
|
}
|
|
if (ctx.qry.difftype) {
|
|
html(delim);
|
|
htmlf("dt=%d", ctx.qry.difftype);
|
|
delim = "&";
|
|
}
|
|
if (ctx.qry.context > 0 && ctx.qry.context != 3) {
|
|
html(delim);
|
|
html("context=");
|
|
htmlf("%d", ctx.qry.context);
|
|
delim = "&";
|
|
}
|
|
if (ctx.qry.ignorews) {
|
|
html(delim);
|
|
html("ignorews=1");
|
|
delim = "&";
|
|
}
|
|
if (ctx.qry.follow) {
|
|
html(delim);
|
|
html("follow=1");
|
|
}
|
|
html("'>");
|
|
if (name[0] != '\0') {
|
|
if (strlen(name) > ctx.cfg.max_msg_len && ctx.cfg.max_msg_len >= 15) {
|
|
html_ntxt(name, ctx.cfg.max_msg_len - 3);
|
|
html("...");
|
|
} else
|
|
html_txt(name);
|
|
} else
|
|
html_txt("(no commit message)");
|
|
html("</a>");
|
|
}
|
|
|
|
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_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)
|
|
{
|
|
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 = "&";
|
|
}
|
|
if (old_rev) {
|
|
html(delim);
|
|
html("id2=");
|
|
html_url_arg(old_rev);
|
|
delim = "&";
|
|
}
|
|
if (ctx.qry.difftype) {
|
|
html(delim);
|
|
htmlf("dt=%d", ctx.qry.difftype);
|
|
delim = "&";
|
|
}
|
|
if (ctx.qry.context > 0 && ctx.qry.context != 3) {
|
|
html(delim);
|
|
html("context=");
|
|
htmlf("%d", ctx.qry.context);
|
|
delim = "&";
|
|
}
|
|
if (ctx.qry.ignorews) {
|
|
html(delim);
|
|
html("ignorews=1");
|
|
delim = "&";
|
|
}
|
|
if (ctx.qry.follow) {
|
|
html(delim);
|
|
html("follow=1");
|
|
}
|
|
html("'>");
|
|
html_txt(name);
|
|
html("</a>");
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
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, "about"))
|
|
cgit_about_link(name, title, class, ctx.qry.head,
|
|
ctx.qry.has_oid ? ctx.qry.oid : NULL,
|
|
ctx.qry.path);
|
|
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)
|
|
{
|
|
char *page, *shortrev, *fullrev, *name;
|
|
|
|
fullrev = oid_to_hex(&obj->oid);
|
|
shortrev = xstrdup(fullrev);
|
|
shortrev[10] = '\0';
|
|
if (obj->type == OBJ_COMMIT) {
|
|
cgit_commit_link(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 = fmt("%s %s...", type_name(obj->type), shortrev);
|
|
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;
|
|
}
|
|
|
|
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("<a ");
|
|
if (class)
|
|
htmlf("class='%s' ", class);
|
|
html("href='");
|
|
if (item) {
|
|
html_attrf(item->util, rev);
|
|
} else {
|
|
dir = strrchr(path, '/');
|
|
if (dir)
|
|
dir++;
|
|
else
|
|
dir = path;
|
|
html_attrf(ctx.repo->module_link, dir, rev);
|
|
}
|
|
html("'>");
|
|
html_txt(path);
|
|
html("</a>");
|
|
} else {
|
|
html("<span");
|
|
if (class)
|
|
htmlf(" class='%s'", class);
|
|
html(">");
|
|
html_txt(path);
|
|
html("</span>");
|
|
}
|
|
html_txtf(" @ %.7s", rev);
|
|
if (item && tail)
|
|
path[len - 1] = tail;
|
|
}
|
|
|
|
const 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;
|
|
}
|
|
|
|
static void print_rel_date(time_t t, int tz, double value,
|
|
const char *class, const char *suffix)
|
|
{
|
|
htmlf("<span class='%s' title='", class);
|
|
html_attr(show_date(t, tz, cgit_date_mode(DATE_DOTTIME)));
|
|
htmlf("'>%.0f %s</span>", value, suffix);
|
|
}
|
|
|
|
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 (secs > max_relative && max_relative >= 0) {
|
|
html("<span title='");
|
|
html_attr(show_date(t, tz, cgit_date_mode(DATE_DOTTIME)));
|
|
html("'>");
|
|
html_txt(show_date(t, tz, cgit_date_mode(DATE_SHORT)));
|
|
html("</span>");
|
|
return;
|
|
}
|
|
|
|
if (secs < TM_HOUR * 2) {
|
|
print_rel_date(t, tz, secs * 1.0 / TM_MIN, "age-mins", "min.");
|
|
return;
|
|
}
|
|
if (secs < TM_DAY * 2) {
|
|
print_rel_date(t, tz, secs * 1.0 / TM_HOUR, "age-hours", "hours");
|
|
return;
|
|
}
|
|
if (secs < TM_WEEK * 2) {
|
|
print_rel_date(t, tz, secs * 1.0 / TM_DAY, "age-days", "days");
|
|
return;
|
|
}
|
|
if (secs < TM_MONTH * 2) {
|
|
print_rel_date(t, tz, secs * 1.0 / TM_WEEK, "age-weeks", "weeks");
|
|
return;
|
|
}
|
|
if (secs < TM_YEAR * 2) {
|
|
print_rel_date(t, tz, secs * 1.0 / TM_MONTH, "age-months", "months");
|
|
return;
|
|
}
|
|
print_rel_date(t, tz, secs * 1.0 / TM_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");
|
|
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");
|
|
}
|
|
|
|
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");
|
|
}
|
|
|
|
void cgit_print_docstart(void)
|
|
{
|
|
char *host = cgit_hosturl();
|
|
|
|
if (ctx.cfg.embedded) {
|
|
if (ctx.cfg.header)
|
|
html_include(ctx.cfg.header);
|
|
return;
|
|
}
|
|
|
|
html(cgit_doctype);
|
|
html("<html lang='en'>\n");
|
|
html("<head>\n");
|
|
html("<title>");
|
|
html_txt(ctx.page.title);
|
|
html("</title>\n");
|
|
htmlf("<meta name='generator' content='cgit %s'/>\n", cgit_version);
|
|
if (ctx.cfg.robots && *ctx.cfg.robots)
|
|
htmlf("<meta name='robots' content='%s'/>\n", ctx.cfg.robots);
|
|
html("<link rel='stylesheet' type='text/css' href='");
|
|
html_attr(ctx.cfg.css);
|
|
html("'/>\n");
|
|
if (ctx.cfg.favicon) {
|
|
html("<link rel='shortcut icon' href='");
|
|
html_attr(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("<link rel='alternate' title='Atom feed' href='");
|
|
html(cgit_httpscheme());
|
|
html_attr(host);
|
|
fileurl = cgit_fileurl(ctx.repo->url, "atom", ctx.qry.vpath,
|
|
sb.buf);
|
|
html_attr(fileurl);
|
|
html("' type='application/atom+xml'/>\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("</head>\n");
|
|
html("<body>\n");
|
|
if (ctx.cfg.header)
|
|
html_include(ctx.cfg.header);
|
|
free(host);
|
|
}
|
|
|
|
void cgit_print_docend(void)
|
|
{
|
|
html("</div> <!-- class=content -->\n");
|
|
if (ctx.cfg.embedded) {
|
|
html("</div> <!-- id=cgit -->\n");
|
|
if (ctx.cfg.footer)
|
|
html_include(ctx.cfg.footer);
|
|
return;
|
|
}
|
|
if (ctx.cfg.footer)
|
|
html_include(ctx.cfg.footer);
|
|
else {
|
|
htmlf("<div class='footer'>generated by <a href='https://git.causal.agency/cgit-pink/about/'>cgit-pink %s</a> "
|
|
"(<a href='https://git-scm.com/'>git %s</a>) at ", cgit_version, git_version_string);
|
|
html_txt(show_date(time(NULL), 0, cgit_date_mode(DATE_DOTTIME)));
|
|
html("</div>\n");
|
|
}
|
|
html("</div> <!-- id=cgit -->\n");
|
|
html("</body>\n</html>\n");
|
|
}
|
|
|
|
void cgit_print_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();
|
|
va_start(ap, fmt);
|
|
cgit_vprint_error(fmt, ap);
|
|
va_end(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();
|
|
}
|
|
|
|
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)
|
|
add_clone_urls(fn, 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 int print_this_commit_option(void)
|
|
{
|
|
struct object_id oid;
|
|
if (!ctx.qry.head || get_oid(ctx.qry.head, &oid))
|
|
return 1;
|
|
html_option(oid_to_hex(&oid), "this commit", ctx.qry.head);
|
|
return 0;
|
|
}
|
|
|
|
static int print_branch_option(const char *refname, const struct object_id *oid,
|
|
int flags, void *cb_data)
|
|
{
|
|
char *name = (char *)refname;
|
|
html_option(name, name, ctx.qry.head);
|
|
return 0;
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
static const char *hc(const char *page)
|
|
{
|
|
if (!ctx.qry.page)
|
|
return NULL;
|
|
|
|
return strcmp(ctx.qry.page, page) ? NULL : "active";
|
|
}
|
|
|
|
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)
|
|
{
|
|
char *logo = NULL, *logo_link = NULL;
|
|
|
|
html("<table id='header'>\n");
|
|
html("<tr>\n");
|
|
|
|
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;
|
|
if (logo && *logo) {
|
|
html("<td class='logo' rowspan='2'><a 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></td>\n");
|
|
}
|
|
|
|
html("<td class='main'>");
|
|
if (ctx.repo) {
|
|
cgit_summary_link(ctx.repo->name, NULL, NULL, NULL);
|
|
if (ctx.env.authenticated) {
|
|
html("</td><td class='form'>");
|
|
html("<form method='get'>\n");
|
|
cgit_add_hidden_formfields(0, 1, ctx.qry.page);
|
|
html("<select name='h' onchange='this.form.submit();'>\n");
|
|
print_this_commit_option();
|
|
html("<optgroup label='branches'>");
|
|
for_each_branch_ref(print_branch_option, ctx.qry.head);
|
|
if (ctx.repo->enable_remote_branches)
|
|
for_each_remote_ref(print_branch_option, ctx.qry.head);
|
|
html("</optgroup>");
|
|
html("</select> ");
|
|
html("<input type='submit' value='switch'/>");
|
|
html("</form>");
|
|
}
|
|
} else
|
|
html_txt(ctx.cfg.root_title);
|
|
html("</td></tr>\n");
|
|
|
|
html("<tr><td class='sub'>");
|
|
if (ctx.repo) {
|
|
html_txt(ctx.repo->desc);
|
|
html("</td><td class='sub right'>");
|
|
if (ctx.repo->owner_filter) {
|
|
cgit_open_filter(ctx.repo->owner_filter);
|
|
html_txt(ctx.repo->owner);
|
|
cgit_close_filter(ctx.repo->owner_filter);
|
|
} else {
|
|
html_txt(ctx.repo->owner);
|
|
}
|
|
} else {
|
|
if (ctx.cfg.root_desc)
|
|
html_txt(ctx.cfg.root_desc);
|
|
}
|
|
html("</td></tr></table>\n");
|
|
}
|
|
|
|
void cgit_print_pageheader(void)
|
|
{
|
|
html("<div id='cgit'>");
|
|
if (!ctx.env.authenticated || !ctx.cfg.noheader)
|
|
print_header();
|
|
|
|
html("<table class='tabs'><tr><td>\n");
|
|
if (ctx.env.authenticated && ctx.repo) {
|
|
if (ctx.repo->readme.nr) {
|
|
cgit_about_link("about", NULL, hc("about"), ctx.qry.head,
|
|
ctx.qry.oid, ctx.qry.vpath);
|
|
html(" ");
|
|
}
|
|
cgit_summary_link("summary", NULL, hc("summary"),
|
|
ctx.qry.head);
|
|
html(" ");
|
|
cgit_refs_link("refs", NULL, hc("refs"), ctx.qry.head,
|
|
ctx.qry.oid, NULL);
|
|
html(" ");
|
|
cgit_log_link("log", NULL, hc("log"), ctx.qry.head,
|
|
NULL, ctx.qry.vpath, 0, NULL, NULL,
|
|
ctx.qry.showmsg, ctx.qry.follow);
|
|
html(" ");
|
|
if (ctx.qry.page && !strcmp(ctx.qry.page, "blame"))
|
|
cgit_blame_link("blame", NULL, hc("blame"), ctx.qry.head,
|
|
ctx.qry.oid, ctx.qry.vpath);
|
|
else
|
|
cgit_tree_link("tree", NULL, hc("tree"), ctx.qry.head,
|
|
ctx.qry.oid, ctx.qry.vpath);
|
|
html(" ");
|
|
cgit_commit_link("commit", NULL, hc("commit"),
|
|
ctx.qry.head, ctx.qry.oid, ctx.qry.vpath);
|
|
html(" ");
|
|
cgit_diff_link("diff", NULL, hc("diff"), ctx.qry.head,
|
|
ctx.qry.oid, ctx.qry.oid2, ctx.qry.vpath);
|
|
if (ctx.repo->max_stats) {
|
|
html(" ");
|
|
cgit_stats_link("stats", NULL, hc("stats"),
|
|
ctx.qry.head, ctx.qry.vpath);
|
|
}
|
|
if (ctx.repo->homepage) {
|
|
html(" <a href='");
|
|
html_attr(ctx.repo->homepage);
|
|
html("'>homepage</a>");
|
|
}
|
|
html("</td><td class='form'>");
|
|
html("<form class='right' 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'>\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' size='10' name='q' value='");
|
|
html_attr(ctx.qry.search);
|
|
html("'/>\n");
|
|
html("<input type='submit' value='search'/>\n");
|
|
html("</form>\n");
|
|
} else if (ctx.env.authenticated) {
|
|
char *currenturl = cgit_currenturl();
|
|
site_link(NULL, "index", NULL, hc("repolist"), NULL, NULL, 0, 1);
|
|
if (ctx.cfg.root_readme)
|
|
site_link("about", "about", NULL, hc("about"),
|
|
NULL, NULL, 0, 1);
|
|
html("</td><td class='form'>");
|
|
html("<form method='get' action='");
|
|
html_attr(currenturl);
|
|
html("'>\n");
|
|
html("<input type='search' name='q' size='10' value='");
|
|
html_attr(ctx.qry.search);
|
|
html("'/>\n");
|
|
html("<input type='submit' value='search'/>\n");
|
|
html("</form>");
|
|
free(currenturl);
|
|
}
|
|
html("</td></tr></table>\n");
|
|
if (ctx.env.authenticated && ctx.repo && ctx.qry.vpath) {
|
|
html("<div class='path'>");
|
|
html("path: ");
|
|
cgit_print_path_crumbs(ctx.qry.vpath);
|
|
if (ctx.cfg.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);
|
|
ctx.qry.follow = !ctx.qry.follow;
|
|
html(")");
|
|
}
|
|
html("</div>");
|
|
}
|
|
html("<div class='content'>");
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
void cgit_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.
|
|
*/
|
|
if (get_oid(fmt("refs/tags/%s", ref), &oid) == 0 &&
|
|
(ref[0] == 'v' || ref[0] == 'V') && isdigit(ref[1]) &&
|
|
((get_oid(fmt("refs/tags/%s", ref + 1), &oid) == 0) +
|
|
(get_oid(fmt("refs/tags/v%s", ref + 1), &oid) == 0) +
|
|
(get_oid(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
|
|
cgit_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);
|
|
cgit_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,
|
|
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));
|
|
strbuf_addstr(&filename, ".tar.asc");
|
|
html(" (");
|
|
cgit_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;
|
|
|
|
last_slash = path + strlen(path);
|
|
for (slash = last_slash; slash > path; --slash) {
|
|
if (*slash != '/') continue;
|
|
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);
|
|
}
|