Compare commits

..

10 Commits

Author SHA1 Message Date
Samuel Lidén Borell
00ecfaadea config: make empty js= omit script tag
According to the cgitrc man page, an empty js= value should cause the
script tag to be omitted. But instead, a script tag with an empty URL
is emitted. The same applies to css. So, skip emitting a tag if the
specified string is empty.

Signed-off-by: Samuel Lidén Borell <samuel@kodafritt.se>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2023-01-10 17:52:54 +01:00
Andy Green
907134b7a2 js: add dynamic age update
This patch updates the emitted "ages" dynamically on the client side.

After updating on completion of the document load, it sets a timer
to update according to the smallest age it found.  If there are any
ages listed in minutes, then it will update again in 10s.  When the
most recent age is in hours, it updates every 5m.  If days, then
every 30m and so on.

This keeps the cost of the dynamic updates at worst once per 10s.
The updates are done entirely on the client side without contact
with the server.

To make this work reliably, since parsing datetimes is unreliable in
browser js, the unix time is added as an attribute to all age spans.

To make that reliable cross-platform, the unix time is treated as a
uint64_t when it is formatted for printing.

The rules for display conversion of the age is aligned with the
existing server-side rules in ui-shared.h.

If the client or server-side time are not synchronized by ntpd etc,
ages shown on the client will not relate to the original ages computed
at the server.  The client updates the ages immediately when the
DOM has finished loading, so in the case the times at the server and
client are not aligned, this patch changes what the user sees on the
page to reflect patch age compared to client time.

If the server and client clocks are aligned, this patch makes no
difference to what is seen on the page.

Signed-off-by: Andy Green <andy@warmcat.com>
Signed-off-by: Christian Hesse <mail@eworm.de>
2022-12-19 16:50:23 +01:00
Andy Green
aee39b4e9a config: add js
Just like the config allows setting css URL path, add a config for
setting the js URL path

Signed-off-by: Andy Green <andy@warmcat.com>
Reviewed-by: John Keeping <john@keeping.me.uk>
Signed-off-by: Christian Hesse <mail@eworm.de>
2022-12-19 16:50:21 +01:00
Andy Green
093ac96970 css: change to be a list
Without changing the default behaviour of including
/cgit.css if nothing declared, allow the "css" config
to be given multiple times listing one or more
alternative URL paths to be included in the document
head area.

Signed-off-by: Andy Green <andy@warmcat.com>
Signed-off-by: Christian Hesse <mail@eworm.de>
2022-12-19 16:49:09 +01:00
Christian Hesse
91f25909b9 cgitrc: handle value "0" for max-repo-count
Setting max-repo-count to "0" makes cgit loop forever generating page
links. Make this a special value to show all repositories.

Signed-off-by: Christian Hesse <mail@eworm.de>
2022-12-19 16:22:26 +01:00
Hristo Venev
852cb3b0e2 cache: tolerate short writes in print_slot
sendfile() can return after a short read/write, so we may need to call
it more than once. As suggested in the manual page, we fall back to
read/write if sendfile fails with EINVAL or ENOSYS.

On the read/write path, use write_in_full which deals with short writes.

Signed-off-by: Hristo Venev <hristo@venev.name>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2022-12-19 16:13:58 +01:00
John Keeping
4c520cefc9 global: use release_commit_memory()
Instead of calling two separate Git functions to free memory associated
with a commit object, use Git's wrapper which does this.  This also
counts as a potential future bug fix since release_commit_memory() also
resets the parsed state of the commit, meaning any attempt to use it in
the future will correctly fill out the fields again.

release_commit_memory() does not set parents to zero, so keep that for
additional safety in case CGit checks this without calling
parse_commit() again.

Signed-off-by: John Keeping <john@keeping.me.uk>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2022-12-19 16:13:58 +01:00
John Keeping
d071f28cfa css: reset font size for blame oid
In Firefox, the hashes in the blame UI are out of step with the line
number and content leading to ever increasing vertical misalignment.

This is caused by the .oid class setting font-size to 90%, so override
this back to 100% for the blame case, bringing the height of lines in
all three columns of the table back into step.

Signed-off-by: John Keeping <john@keeping.me.uk>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2022-12-19 16:13:58 +01:00
John Keeping
c1a1d23111 ui-blame: add a link to the parent commit in blame
When walking through the history, it is useful to quickly see the same
file at the previous revision, so add a link to do this.

It would be nice to link to the correct line with an additional
fragment, but this requires significantly more work so it can be done as
an enhancement later.  (ent->s_lno is mostly the right thing, but it is
the line number in the post-image of the target commit whereas the link
is to the parent of that commit, i.e. the pre-image of the target.)

Suggested-by: Alejandro Colomar <alx.manpages@gmail.com>
Signed-off-by: John Keeping <john@keeping.me.uk>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2022-12-19 16:13:58 +01:00
Christian Hesse
a0f6669bdb about: allow to give head from query
Reading the README from repository used to be limited to default
branch or a branch given in configuration. Let's allow a branch
from query if not specified explicitly.

Signed-off-by: Christian Hesse <mail@eworm.de>
2022-12-19 16:13:58 +01:00
12 changed files with 184 additions and 47 deletions

View File

@ -87,6 +87,7 @@ install: all
$(INSTALL) -m 0755 cgit $(DESTDIR)$(CGIT_SCRIPT_PATH)/$(CGIT_SCRIPT_NAME)
$(INSTALL) -m 0755 -d $(DESTDIR)$(CGIT_DATA_PATH)
$(INSTALL) -m 0644 cgit.css $(DESTDIR)$(CGIT_DATA_PATH)/cgit.css
$(INSTALL) -m 0644 cgit.js $(DESTDIR)$(CGIT_DATA_PATH)/cgit.js
$(INSTALL) -m 0644 cgit.png $(DESTDIR)$(CGIT_DATA_PATH)/cgit.png
$(INSTALL) -m 0644 favicon.ico $(DESTDIR)$(CGIT_DATA_PATH)/favicon.ico
$(INSTALL) -m 0644 robots.txt $(DESTDIR)$(CGIT_DATA_PATH)/robots.txt

39
cache.c
View File

@ -85,40 +85,45 @@ static int close_slot(struct cache_slot *slot)
/* Print the content of the active cache slot (but skip the key). */
static int print_slot(struct cache_slot *slot)
{
off_t off;
#ifdef HAVE_LINUX_SENDFILE
off_t start_off;
int ret;
off_t size;
#endif
start_off = slot->keylen + 1;
off = slot->keylen + 1;
#ifdef HAVE_LINUX_SENDFILE
size = slot->cache_st.st_size;
do {
ret = sendfile(STDOUT_FILENO, slot->cache_fd, &start_off,
slot->cache_st.st_size - start_off);
ssize_t ret;
ret = sendfile(STDOUT_FILENO, slot->cache_fd, &off, size - off);
if (ret < 0) {
if (errno == EAGAIN || errno == EINTR)
continue;
/* Fall back to read/write on EINVAL or ENOSYS */
if (errno == EINVAL || errno == ENOSYS)
break;
return errno;
}
if (off == size)
return 0;
} while (1);
#else
ssize_t i, j;
#endif
i = lseek(slot->cache_fd, slot->keylen + 1, SEEK_SET);
if (i != slot->keylen + 1)
if (lseek(slot->cache_fd, off, SEEK_SET) != off)
return errno;
do {
i = j = xread(slot->cache_fd, slot->buf, sizeof(slot->buf));
if (i > 0)
j = xwrite(STDOUT_FILENO, slot->buf, i);
} while (i > 0 && j == i);
if (i < 0 || j != i)
ssize_t ret;
ret = xread(slot->cache_fd, slot->buf, sizeof(slot->buf));
if (ret < 0)
return errno;
else
if (ret == 0)
return 0;
#endif
if (write_in_full(STDOUT_FILENO, slot->buf, ret) < 0)
return errno;
} while (1);
}
/* Check if the slot has expired */

19
cgit.c
View File

@ -142,7 +142,9 @@ static void config_cb(const char *name, const char *value)
else if (!strcmp(name, "root-readme"))
ctx.cfg.root_readme = xstrdup(value);
else if (!strcmp(name, "css"))
ctx.cfg.css = xstrdup(value);
string_list_append(&ctx.cfg.css, xstrdup(value));
else if (!strcmp(name, "js"))
string_list_append(&ctx.cfg.js, xstrdup(value));
else if (!strcmp(name, "favicon"))
ctx.cfg.favicon = xstrdup(value);
else if (!strcmp(name, "footer"))
@ -237,9 +239,11 @@ static void config_cb(const char *name, const char *value)
ctx.cfg.max_repodesc_len = atoi(value);
else if (!strcmp(name, "max-blob-size"))
ctx.cfg.max_blob_size = atoi(value);
else if (!strcmp(name, "max-repo-count"))
else if (!strcmp(name, "max-repo-count")) {
ctx.cfg.max_repo_count = atoi(value);
else if (!strcmp(name, "max-commit-count"))
if (ctx.cfg.max_repo_count <= 0)
ctx.cfg.max_repo_count = INT_MAX;
} else if (!strcmp(name, "max-commit-count"))
ctx.cfg.max_commit_count = atoi(value);
else if (!strcmp(name, "project-list"))
ctx.cfg.project_list = xstrdup(expand_macros(value));
@ -376,7 +380,6 @@ static void prepare_context(void)
ctx.cfg.case_sensitive_sort = 1;
ctx.cfg.branch_sort = 0;
ctx.cfg.commit_sort = 0;
ctx.cfg.css = "/cgit.css";
ctx.cfg.logo = "/cgit.png";
ctx.cfg.favicon = "/favicon.ico";
ctx.cfg.local_time = 0;
@ -507,9 +510,11 @@ static inline void parse_readme(const char *readme, char **filename, char **ref,
/* Check if the readme is tracked in the git repo. */
colon = strchr(readme, ':');
if (colon && strlen(colon) > 1) {
/* If it starts with a colon, we want to use
* the default branch */
if (colon == readme && repo->defbranch)
/* If it starts with a colon, we want to use head given
* from query or the default branch */
if (colon == readme && ctx.qry.head)
*ref = xstrdup(ctx.qry.head);
else if (colon == readme && repo->defbranch)
*ref = xstrdup(repo->defbranch);
else
*ref = xstrndup(readme, colon - readme);

View File

@ -363,6 +363,10 @@ div#cgit table.blame td.lines > div > pre {
top: 0;
}
div#cgit table.blame .oid {
font-size: 100%;
}
div#cgit table.bin-blob {
margin-top: 0.5em;
border: solid 1px black;

4
cgit.h
View File

@ -25,6 +25,7 @@
#include <utf8.h>
#include <notes.h>
#include <graph.h>
#include <inttypes.h>
/* Add isgraph(x) to Git's sane ctype support (see git-compat-util.h) */
#undef isgraph
@ -195,7 +196,6 @@ struct cgit_config {
char *cache_root;
char *clone_prefix;
char *clone_url;
char *css;
char *favicon;
char *footer;
char *head_include;
@ -206,6 +206,7 @@ struct cgit_config {
char *module_link;
char *project_list;
struct string_list readme;
struct string_list css;
char *robots;
char *root_title;
char *root_desc;
@ -264,6 +265,7 @@ struct cgit_config {
int branch_sort;
int commit_sort;
struct string_list mimetypes;
struct string_list js;
struct cgit_filter *about_filter;
struct cgit_filter *commit_filter;
struct cgit_filter *source_filter;

68
cgit.js Normal file
View File

@ -0,0 +1,68 @@
/* cgit.js: javacript functions for cgit
*
* Copyright (C) 2006-2018 cgit Development Team <cgit@lists.zx2c4.com>
*
* Licensed under GNU General Public License v2
* (see COPYING for full license text)
*/
(function () {
/* This follows the logic and suffixes used in ui-shared.c */
var age_classes = [ "age-mins", "age-hours", "age-days", "age-weeks", "age-months", "age-years" ];
var age_suffix = [ "min.", "hours", "days", "weeks", "months", "years", "years" ];
var age_next = [ 60, 3600, 24 * 3600, 7 * 24 * 3600, 30 * 24 * 3600, 365 * 24 * 3600, 365 * 24 * 3600 ];
var age_limit = [ 7200, 24 * 7200, 7 * 24 * 7200, 30 * 24 * 7200, 365 * 25 * 7200, 365 * 25 * 7200 ];
var update_next = [ 10, 5 * 60, 1800, 24 * 3600, 24 * 3600, 24 * 3600, 24 * 3600 ];
function render_age(e, age) {
var t, n;
for (n = 0; n < age_classes.length; n++)
if (age < age_limit[n])
break;
t = Math.round(age / age_next[n]) + " " + age_suffix[n];
if (e.textContent != t) {
e.textContent = t;
if (n == age_classes.length)
n--;
if (e.className != age_classes[n])
e.className = age_classes[n];
}
}
function aging() {
var n, next = 24 * 3600,
now_ut = Math.round((new Date().getTime() / 1000));
for (n = 0; n < age_classes.length; n++) {
var m, elems = document.getElementsByClassName(age_classes[n]);
if (elems.length && update_next[n] < next)
next = update_next[n];
for (m = 0; m < elems.length; m++) {
var age = now_ut - elems[m].getAttribute("data-ut");
render_age(elems[m], age);
}
}
/*
* We only need to come back when the age might have changed.
* Eg, if everything is counted in hours already, once per
* 5 minutes is accurate enough.
*/
window.setTimeout(aging, next * 1000);
}
document.addEventListener("DOMContentLoaded", function() {
/* we can do the aging on DOM content load since no layout dependency */
aging();
}, false);
})();

View File

@ -126,7 +126,8 @@ commit-sort::
css::
Url which specifies the css document to include in all cgit pages.
Default value: "/cgit.css".
Default value: "/cgit.css". May be given multiple times, each
css URL path is added in the head section of the document in turn.
email-filter::
Specifies a command which will be invoked to format names and email
@ -238,6 +239,11 @@ include::
Name of a configfile to include before the rest of the current config-
file is parsed. Default value: none. See also: "MACRO EXPANSION".
js::
Url which specifies the javascript script document to include in all cgit
pages. Default value: "/cgit.js". Setting this to an empty string will
disable generation of the link to this file in the head section.
local-time::
Flag which, if set to "1", makes cgit print commit and tag times in the
servers timezone. Default value: "0".
@ -269,7 +275,8 @@ max-message-length::
max-repo-count::
Specifies the number of entries to list per page on the repository
index page. Default value: "50".
index page. The value "0" shows all repositories without limitation.
Default value: "50".
max-repodesc-length::
Specifies the maximum number of repo description characters to display
@ -579,11 +586,11 @@ repo.readme::
verbatim as the "About" page for this repo. You may also specify a
git refspec by head or by hash by prepending the refspec followed by
a colon. For example, "master:docs/readme.mkd". If the value begins
with a colon, i.e. ":docs/readme.rst", the default branch of the
repository will be used. Sharing any file will expose that entire
directory tree to the "/about/PATH" endpoints, so be sure that there
are no non-public files located in the same directory as the readme
file. Default value: <readme>.
with a colon, i.e. ":docs/readme.rst", the head giving in query or
the default branch of the repository will be used. Sharing any file
will expose that entire directory tree to the "/about/PATH" endpoints,
so be sure that there are no non-public files located in the same
directory as the readme file. Default value: <readme>.
repo.section::
Override the current section name for this repository. Default value:

View File

@ -149,8 +149,7 @@ void cgit_print_atom(char *tip, const char *path, int max_count)
first = false;
}
add_entry(commit, host);
free_commit_buffer(the_repository->parsed_objects, commit);
free_commit_list(commit->parents);
release_commit_memory(the_repository->parsed_objects, commit);
commit->parents = NULL;
}
html("</feed>\n");

View File

@ -54,6 +54,15 @@ static void emit_blame_entry_hash(struct blame_entry *ent)
html("</span>");
free(detail);
if (!parse_commit(suspect->commit) && suspect->commit->parents) {
struct commit *parent = suspect->commit->parents->item;
html(" ");
cgit_blame_link("^", "Blame the previous revision", NULL,
ctx.qry.head, oid_to_hex(&parent->object.oid),
suspect->path);
}
while (line++ < ent->num_lines)
html("\n");
}

View File

@ -489,8 +489,7 @@ void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern
for (i = 0; i < ofs && (commit = get_revision(&rev)) != NULL; /* nop */) {
if (show_commit(commit, &rev))
i++;
free_commit_buffer(the_repository->parsed_objects, commit);
free_commit_list(commit->parents);
release_commit_memory(the_repository->parsed_objects, commit);
commit->parents = NULL;
}
@ -511,8 +510,7 @@ void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern
i++;
print_commit(commit, &rev);
}
free_commit_buffer(the_repository->parsed_objects, commit);
free_commit_list(commit->parents);
release_commit_memory(the_repository->parsed_objects, commit);
commit->parents = NULL;
}
if (pager) {

View File

@ -673,7 +673,7 @@ const struct date_mode *cgit_date_mode(enum date_mode_type type)
static void print_rel_date(time_t t, int tz, double value,
const char *class, const char *suffix)
{
htmlf("<span class='%s' title='", class);
htmlf("<span class='%s' data-ut='%" PRIu64 "' title='", class, (uint64_t)t);
html_attr(show_date(t, tz, cgit_date_mode(DATE_ISO8601)));
htmlf("'>%.0f %s</span>", value, suffix);
}
@ -768,6 +768,38 @@ static void print_rel_vcs_link(const char *url)
html(" Git repository'/>\n");
}
static int emit_css_link(struct string_list_item *s, void *arg)
{
/* Do not emit anything if css= is specified. */
if (s && *s->string == '\0')
return 0;
html("<link rel='stylesheet' type='text/css' href='");
if (s)
html_attr(s->string);
else
html_attr((const char *)arg);
html("'/>\n");
return 0;
}
static int emit_js_link(struct string_list_item *s, void *arg)
{
/* Do not emit anything if js= is specified. */
if (s && *s->string == '\0')
return 0;
html("<script type='text/javascript' src='");
if (s)
html_attr(s->string);
else
html_attr((const char *)arg);
html("'></script>\n");
return 0;
}
void cgit_print_docstart(void)
{
char *host = cgit_hosturl();
@ -787,9 +819,17 @@ void cgit_print_docstart(void)
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.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) {
html("<link rel='shortcut icon' href='");
html_attr(ctx.cfg.favicon);

View File

@ -241,8 +241,7 @@ static struct string_list collect_stats(const struct cgit_period *period)
memset(&authors, 0, sizeof(authors));
while ((commit = get_revision(&rev)) != NULL) {
add_commit(&authors, commit, period);
free_commit_buffer(the_repository->parsed_objects, commit);
free_commit_list(commit->parents);
release_commit_memory(the_repository->parsed_objects, commit);
commit->parents = NULL;
}
return authors;