From f86a23ff537258d36bf8f1876fa7a4bede6673d8 Mon Sep 17 00:00:00 2001 From: Lars Hjemli <hjemli@gmail.com> Date: Sat, 6 Dec 2008 17:38:19 +0100 Subject: Add a 'stats' page to each repo This new page, which is disabled by default, can be used to print some statistics about the number of commits per period in the repository, where period can be either weeks, months, quarters or years. The function can be activated globally by setting 'enable-stats=1' in cgitrc and disabled for individual repos by setting 'repo.enable-stats=0'. Signed-off-by: Lars Hjemli <hjemli@gmail.com> diff --git a/Makefile b/Makefile index 561af76..f426f98 100644 --- a/Makefile +++ b/Makefile @@ -68,6 +68,7 @@ OBJECTS += ui-refs.o OBJECTS += ui-repolist.o OBJECTS += ui-shared.o OBJECTS += ui-snapshot.o +OBJECTS += ui-stats.o OBJECTS += ui-summary.o OBJECTS += ui-tag.o OBJECTS += ui-tree.o diff --git a/cgit.c b/cgit.c index c82587b..22b6d7c 100644 --- a/cgit.c +++ b/cgit.c @@ -54,6 +54,8 @@ void config_cb(const char *name, const char *value) ctx.cfg.enable_log_filecount = atoi(value); else if (!strcmp(name, "enable-log-linecount")) ctx.cfg.enable_log_linecount = atoi(value); + else if (!strcmp(name, "enable-stats")) + ctx.cfg.enable_stats = atoi(value); else if (!strcmp(name, "cache-size")) ctx.cfg.cache_size = atoi(value); else if (!strcmp(name, "cache-root")) @@ -112,6 +114,8 @@ void config_cb(const char *name, const char *value) ctx.repo->enable_log_filecount = ctx.cfg.enable_log_filecount * atoi(value); else if (ctx.repo && !strcmp(name, "repo.enable-log-linecount")) ctx.repo->enable_log_linecount = ctx.cfg.enable_log_linecount * atoi(value); + else if (ctx.repo && !strcmp(name, "repo.enable-stats")) + ctx.repo->enable_stats = ctx.cfg.enable_stats && atoi(value); else if (ctx.repo && !strcmp(name, "repo.module-link")) ctx.repo->module_link= xstrdup(value); else if (ctx.repo && !strcmp(name, "repo.readme") && value != NULL) { @@ -154,6 +158,8 @@ static void querystring_cb(const char *name, const char *value) ctx.qry.name = xstrdup(value); } else if (!strcmp(name, "mimetype")) { ctx.qry.mimetype = xstrdup(value); + } else if (!strcmp(name, "period")) { + ctx.qry.period = xstrdup(value); } } diff --git a/cgit.css b/cgit.css index a37d218..ef30fbf 100644 --- a/cgit.css +++ b/cgit.css @@ -456,3 +456,80 @@ div.footer { font-size: 80%; color: #ccc; } +table.stats { + border: solid 1px black; + border-collapse: collapse; +} + +table.stats th { + text-align: left; + padding: 1px 0.5em; + background-color: #eee; + border: solid 1px black; +} + +table.stats td { + text-align: right; + padding: 1px 0.5em; + border: solid 1px black; +} + +table.stats td.total { + font-weight: bold; + text-align: left; +} + +table.stats td.sum { + color: #c00; + font-weight: bold; +/* background-color: #eee; */ +} + +table.stats td.left { + text-align: left; +} + +table.vgraph { + border-collapse: separate; + border: solid 1px black; + height: 200px; +} + +table.vgraph th { + background-color: #eee; + font-weight: bold; + border: solid 1px white; + padding: 1px 0.5em; +} + +table.vgraph td { + vertical-align: bottom; + padding: 0px 10px; +} + +table.vgraph div.bar { + background-color: #eee; +} + +table.hgraph { + border: solid 1px black; + width: 800px; +} + +table.hgraph th { + background-color: #eee; + font-weight: bold; + border: solid 1px black; + padding: 1px 0.5em; +} + +table.hgraph td { + vertical-align: center; + padding: 2px 2px; +} + +table.hgraph div.bar { + background-color: #eee; + height: 1em; +} + diff --git a/cgit.h b/cgit.h index 91db98a..85045c4 100644 --- a/cgit.h +++ b/cgit.h @@ -61,6 +61,7 @@ struct cgit_repo { int snapshots; int enable_log_filecount; int enable_log_linecount; + int enable_stats; }; struct cgit_repolist { @@ -119,6 +120,7 @@ struct cgit_query { char *name; char *mimetype; char *url; + char *period; int ofs; int nohead; }; @@ -151,6 +153,7 @@ struct cgit_config { int enable_index_links; int enable_log_filecount; int enable_log_linecount; + int enable_stats; int local_time; int max_repo_count; int max_commit_count; diff --git a/cgitrc.5.txt b/cgitrc.5.txt index 7887b02..60d3ea4 100644 --- a/cgitrc.5.txt +++ b/cgitrc.5.txt @@ -74,6 +74,10 @@ enable-log-linecount and removed lines for each commit on the repository log page. Default value: "0". +enable-stats + Globally enable/disable statistics for each repository. Default + value: "0". + favicon Url used as link to a shortcut icon for cgit. If specified, it is suggested to use the value "/favicon.ico" since certain browsers will @@ -218,6 +222,10 @@ repo.enable-log-linecount A flag which can be used to disable the global setting `enable-log-linecount'. Default value: none. +repo.enable-stats + A flag which can be used to disable the global setting + `enable-stats'. Default value: none. + repo.name The value to show as repository name. Default value: <repo.url>. diff --git a/cmd.c b/cmd.c index 5b3c14c..744bf84 100644 --- a/cmd.c +++ b/cmd.c @@ -21,6 +21,7 @@ #include "ui-refs.h" #include "ui-repolist.h" #include "ui-snapshot.h" +#include "ui-stats.h" #include "ui-summary.h" #include "ui-tag.h" #include "ui-tree.h" @@ -109,6 +110,14 @@ static void snapshot_fn(struct cgit_context *ctx) ctx->repo->snapshots, ctx->qry.nohead); } +static void stats_fn(struct cgit_context *ctx) +{ + if (ctx->repo->enable_stats) + cgit_show_stats(ctx); + else + cgit_print_error("Stats disabled for this repo"); +} + static void summary_fn(struct cgit_context *ctx) { cgit_print_summary(); @@ -145,6 +154,7 @@ struct cgit_cmd *cgit_get_cmd(struct cgit_context *ctx) def_cmd(refs, 1, 1), def_cmd(repolist, 0, 0), def_cmd(snapshot, 1, 0), + def_cmd(stats, 1, 1), def_cmd(summary, 1, 1), def_cmd(tag, 1, 1), def_cmd(tree, 1, 1), diff --git a/shared.c b/shared.c index f5875e4..37333f0 100644 --- a/shared.c +++ b/shared.c @@ -58,6 +58,7 @@ struct cgit_repo *cgit_add_repo(const char *url) ret->snapshots = ctx.cfg.snapshots; ret->enable_log_filecount = ctx.cfg.enable_log_filecount; ret->enable_log_linecount = ctx.cfg.enable_log_linecount; + ret->enable_stats = ctx.cfg.enable_stats; ret->module_link = ctx.cfg.module_link; ret->readme = NULL; return ret; diff --git a/ui-shared.c b/ui-shared.c index 224e5f3..0e688a0 100644 --- a/ui-shared.c +++ b/ui-shared.c @@ -641,6 +641,9 @@ void cgit_print_pageheader(struct cgit_context *ctx) ctx->qry.head, ctx->qry.sha1); cgit_diff_link("diff", NULL, hc(cmd, "diff"), ctx->qry.head, ctx->qry.sha1, ctx->qry.sha2, NULL); + if (ctx->repo->enable_stats) + reporevlink("stats", "stats", NULL, hc(cmd, "stats"), + ctx->qry.head, NULL, NULL); if (ctx->repo->readme) reporevlink("about", "about", NULL, hc(cmd, "about"), ctx->qry.head, NULL, diff --git a/ui-stats.c b/ui-stats.c new file mode 100644 index 0000000..9150840 --- /dev/null +++ b/ui-stats.c @@ -0,0 +1,380 @@ +#include "cgit.h" +#include "html.h" +#include <string-list.h> + +#define MONTHS 6 + +struct Period { + const char code; + const char *name; + int max_periods; + int count; + + /* Convert a tm value to the first day in the period */ + void (*trunc)(struct tm *tm); + + /* Update tm value to start of next/previous period */ + void (*dec)(struct tm *tm); + void (*inc)(struct tm *tm); + + /* Pretty-print a tm value */ + char *(*pretty)(struct tm *tm); +}; + +struct authorstat { + long total; + struct string_list list; +}; + +#define DAY_SECS (60 * 60 * 24) +#define WEEK_SECS (DAY_SECS * 7) + +static void trunc_week(struct tm *tm) +{ + time_t t = timegm(tm); + t -= ((tm->tm_wday + 6) % 7) * DAY_SECS; + gmtime_r(&t, tm); +} + +static void dec_week(struct tm *tm) +{ + time_t t = timegm(tm); + t -= WEEK_SECS; + gmtime_r(&t, tm); +} + +static void inc_week(struct tm *tm) +{ + time_t t = timegm(tm); + t += WEEK_SECS; + gmtime_r(&t, tm); +} + +static char *pretty_week(struct tm *tm) +{ + static char buf[10]; + + strftime(buf, sizeof(buf), "W%V %G", tm); + return buf; +} + +static void trunc_month(struct tm *tm) +{ + tm->tm_mday = 1; +} + +static void dec_month(struct tm *tm) +{ + tm->tm_mon--; + if (tm->tm_mon < 0) { + tm->tm_year--; + tm->tm_mon = 11; + } +} + +static void inc_month(struct tm *tm) +{ + tm->tm_mon++; + if (tm->tm_mon > 11) { + tm->tm_year++; + tm->tm_mon = 0; + } +} + +static char *pretty_month(struct tm *tm) +{ + static const char *months[] = { + "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" + }; + return fmt("%s %d", months[tm->tm_mon], tm->tm_year + 1900); +} + +static void trunc_quarter(struct tm *tm) +{ + trunc_month(tm); + while(tm->tm_mon % 3 != 0) + dec_month(tm); +} + +static void dec_quarter(struct tm *tm) +{ + dec_month(tm); + dec_month(tm); + dec_month(tm); +} + +static void inc_quarter(struct tm *tm) +{ + inc_month(tm); + inc_month(tm); + inc_month(tm); +} + +static char *pretty_quarter(struct tm *tm) +{ + return fmt("Q%d %d", tm->tm_mon / 3 + 1, tm->tm_year + 1900); +} + +static void trunc_year(struct tm *tm) +{ + trunc_month(tm); + tm->tm_mon = 0; +} + +static void dec_year(struct tm *tm) +{ + tm->tm_year--; +} + +static void inc_year(struct tm *tm) +{ + tm->tm_year++; +} + +static char *pretty_year(struct tm *tm) +{ + return fmt("%d", tm->tm_year + 1900); +} + +struct Period periods[] = { + {'w', "week", 12, 4, trunc_week, dec_week, inc_week, pretty_week}, + {'m', "month", 12, 4, trunc_month, dec_month, inc_month, pretty_month}, + {'q', "quarter", 12, 4, trunc_quarter, dec_quarter, inc_quarter, pretty_quarter}, + {'y', "year", 12, 4, trunc_year, dec_year, inc_year, pretty_year}, +}; + +static void add_commit(struct string_list *authors, struct commit *commit, + struct Period *period) +{ + struct commitinfo *info; + struct string_list_item *author, *item; + struct authorstat *authorstat; + struct string_list *items; + char *tmp; + struct tm *date; + time_t t; + + info = cgit_parse_commit(commit); + tmp = xstrdup(info->author); + author = string_list_insert(tmp, authors); + if (!author->util) + author->util = xcalloc(1, sizeof(struct authorstat)); + else + free(tmp); + authorstat = author->util; + items = &authorstat->list; + t = info->committer_date; + date = gmtime(&t); + period->trunc(date); + tmp = xstrdup(period->pretty(date)); + item = string_list_insert(tmp, items); + if (item->util) + free(tmp); + item->util++; + authorstat->total++; + cgit_free_commitinfo(info); +} + +static int cmp_total_commits(const void *a1, const void *a2) +{ + const struct string_list_item *i1 = a1; + const struct string_list_item *i2 = a2; + const struct authorstat *auth1 = i1->util; + const struct authorstat *auth2 = i2->util; + + return auth2->total - auth1->total; +} + +/* Walk the commit DAG and collect number of commits per author per + * timeperiod into a nested string_list collection. + */ +struct string_list collect_stats(struct cgit_context *ctx, + struct Period *period) +{ + struct string_list authors; + struct rev_info rev; + struct commit *commit; + const char *argv[] = {NULL, ctx->qry.head, NULL, NULL}; + time_t now; + long i; + struct tm *tm; + char tmp[11]; + + time(&now); + tm = gmtime(&now); + period->trunc(tm); + for (i = 1; i < period->count; i++) + period->dec(tm); + strftime(tmp, sizeof(tmp), "%Y-%m-%d", tm); + argv[2] = xstrdup(fmt("--since=%s", tmp)); + init_revisions(&rev, NULL); + rev.abbrev = DEFAULT_ABBREV; + rev.commit_format = CMIT_FMT_DEFAULT; + rev.no_merges = 1; + rev.verbose_header = 1; + rev.show_root_diff = 0; + setup_revisions(3, argv, &rev, NULL); + prepare_revision_walk(&rev); + memset(&authors, 0, sizeof(authors)); + while ((commit = get_revision(&rev)) != NULL) { + add_commit(&authors, commit, period); + free(commit->buffer); + free_commit_list(commit->parents); + } + return authors; +} + +void print_combined_authorrow(struct string_list *authors, int from, int to, + const char *name, const char *leftclass, const char *centerclass, + const char *rightclass, struct Period *period) +{ + struct string_list_item *author; + struct authorstat *authorstat; + struct string_list *items; + struct string_list_item *date; + time_t now; + long i, j, total, subtotal; + struct tm *tm; + char *tmp; + + time(&now); + tm = gmtime(&now); + period->trunc(tm); + for (i = 1; i < period->count; i++) + period->dec(tm); + + total = 0; + htmlf("<tr><td class='%s'>%s</td>", leftclass, + fmt(name, to - from + 1)); + for (j = 0; j < period->count; j++) { + tmp = period->pretty(tm); + period->inc(tm); + subtotal = 0; + for (i = from; i <= to; i++) { + author = &authors->items[i]; + authorstat = author->util; + items = &authorstat->list; + date = string_list_lookup(tmp, items); + if (date) + subtotal += (size_t)date->util; + } + htmlf("<td class='%s'>%d</td>", centerclass, subtotal); + total += subtotal; + } + htmlf("<td class='%s'>%d</td></tr>", rightclass, total); +} + +void print_authors(struct string_list *authors, int top, struct Period *period) +{ + struct string_list_item *author; + struct authorstat *authorstat; + struct string_list *items; + struct string_list_item *date; + time_t now; + long i, j, total; + struct tm *tm; + char *tmp; + + time(&now); + tm = gmtime(&now); + period->trunc(tm); + for (i = 1; i < period->count; i++) + period->dec(tm); + + html("<table class='stats'><tr><th>Author</th>"); + for (j = 0; j < period->count; j++) { + tmp = period->pretty(tm); + htmlf("<th>%s</th>", tmp); + period->inc(tm); + } + html("<th>Total</th></tr>\n"); + + if (top <= 0 || top > authors->nr) + top = authors->nr; + + for (i = 0; i < top; i++) { + author = &authors->items[i]; + html("<tr><td class='left'>"); + html_txt(author->string); + html("</td>"); + authorstat = author->util; + items = &authorstat->list; + total = 0; + for (j = 0; j < period->count; j++) + period->dec(tm); + for (j = 0; j < period->count; j++) { + tmp = period->pretty(tm); + period->inc(tm); + date = string_list_lookup(tmp, items); + if (!date) + html("<td>0</td>"); + else { + htmlf("<td>%d</td>", date->util); + total += (size_t)date->util; + } + } + htmlf("<td class='sum'>%d</td></tr>", total); + } + + if (top < authors->nr) + print_combined_authorrow(authors, top, authors->nr - 1, + "Others (%d)", "left", "", "sum", period); + + print_combined_authorrow(authors, 0, authors->nr - 1, "Total", + "total", "sum", "sum", period); + html("</table>"); +} + +/* Create a sorted string_list with one entry per author. The util-field + * for each author is another string_list which is used to calculate the + * number of commits per time-interval. + */ +void cgit_show_stats(struct cgit_context *ctx) +{ + struct string_list authors; + struct Period *period; + int top, i; + + period = &periods[0]; + if (ctx->qry.period) { + for (i = 0; i < sizeof(periods) / sizeof(periods[0]); i++) + if (periods[i].code == ctx->qry.period[0]) { + period = &periods[i]; + break; + } + } + authors = collect_stats(ctx, period); + qsort(authors.items, authors.nr, sizeof(struct string_list_item), + cmp_total_commits); + + top = ctx->qry.ofs; + if (!top) + top = 10; + htmlf("<h2>Commits per author per %s</h2>", period->name); + + html("<form method='get' action='.' style='float: right; text-align: right;'>"); + if (strcmp(ctx->qry.head, ctx->repo->defbranch)) + htmlf("<input type='hidden' name='h' value='%s'/>", ctx->qry.head); + html("Period: "); + html("<select name='period' onchange='this.form.submit();'>"); + for (i = 0; i < sizeof(periods) / sizeof(periods[0]); i++) + htmlf("<option value='%c'%s>%s</option>", + periods[i].code, + period == &periods[i] ? " selected" : "", + periods[i].name); + html("</select><br/><br/>"); + html("Authors: "); + html(""); + html("<select name='ofs' onchange='this.form.submit();'>"); + htmlf("<option value='10'%s>10</option>", top == 10 ? " selected" : ""); + htmlf("<option value='25'%s>25</option>", top == 25 ? " selected" : ""); + htmlf("<option value='50'%s>50</option>", top == 50 ? " selected" : ""); + htmlf("<option value='100'%s>100</option>", top == 100 ? " selected" : ""); + htmlf("<option value='-1'%s>All</option>", top == -1 ? " selected" : ""); + html("</select>"); + html("<noscript> <input type='submit' value='Reload'/></noscript>"); + html("</form>"); + print_authors(&authors, top, period); +} + diff --git a/ui-stats.h b/ui-stats.h new file mode 100644 index 0000000..f1d744c --- /dev/null +++ b/ui-stats.h @@ -0,0 +1,8 @@ +#ifndef UI_STATS_H +#define UI_STATS_H + +#include "cgit.h" + +extern void cgit_show_stats(struct cgit_context *ctx); + +#endif /* UI_STATS_H */ -- cgit v0.10.1 From c6a6aa2186daf39814baa0e71378c2e9e1041002 Mon Sep 17 00:00:00 2001 From: Lars Hjemli <hjemli@gmail.com> Date: Sun, 7 Dec 2008 11:45:28 +0100 Subject: ui-stats: enable path-filtered stats When a path is specified on the querystring the commit statistics will now be filtered by this path. Signed-off-by: Lars Hjemli <hjemli@gmail.com> diff --git a/ui-stats.c b/ui-stats.c index 9150840..3cc8d70 100644 --- a/ui-stats.c +++ b/ui-stats.c @@ -195,7 +195,8 @@ struct string_list collect_stats(struct cgit_context *ctx, struct string_list authors; struct rev_info rev; struct commit *commit; - const char *argv[] = {NULL, ctx->qry.head, NULL, NULL}; + const char *argv[] = {NULL, ctx->qry.head, NULL, NULL, NULL, NULL}; + int argc = 3; time_t now; long i; struct tm *tm; @@ -208,13 +209,18 @@ struct string_list collect_stats(struct cgit_context *ctx, period->dec(tm); strftime(tmp, sizeof(tmp), "%Y-%m-%d", tm); argv[2] = xstrdup(fmt("--since=%s", tmp)); + if (ctx->qry.path) { + argv[3] = "--"; + argv[4] = ctx->qry.path; + argc += 2; + } init_revisions(&rev, NULL); rev.abbrev = DEFAULT_ABBREV; rev.commit_format = CMIT_FMT_DEFAULT; rev.no_merges = 1; rev.verbose_header = 1; rev.show_root_diff = 0; - setup_revisions(3, argv, &rev, NULL); + setup_revisions(argc, argv, &rev, NULL); prepare_revision_walk(&rev); memset(&authors, 0, sizeof(authors)); while ((commit = get_revision(&rev)) != NULL) { @@ -351,7 +357,13 @@ void cgit_show_stats(struct cgit_context *ctx) top = ctx->qry.ofs; if (!top) top = 10; - htmlf("<h2>Commits per author per %s</h2>", period->name); + htmlf("<h2>Commits per author per %s", period->name); + if (ctx->qry.path) { + html(" (path '"); + html_txt(ctx->qry.path); + html("')"); + } + html("</h2>"); html("<form method='get' action='.' style='float: right; text-align: right;'>"); if (strcmp(ctx->qry.head, ctx->repo->defbranch)) -- cgit v0.10.1 From fb2f3f6c29bad733723152893c5246a756e4cada Mon Sep 17 00:00:00 2001 From: Lars Hjemli <hjemli@gmail.com> Date: Sun, 7 Dec 2008 13:17:21 +0100 Subject: ui-stats: replace 'enable-stats' setting with 'max-stats' The new 'max-stats' and 'repo.max-stats' settings makes it possible to define the maximum statistics period, both globally and per repo. Hence, it is now feasible to allow statistics on repositories with a high commit frequency, like linux-2.6, by setting repo.max-stats to e.g. 'month'. Signed-off-by: Lars Hjemli <hjemli@gmail.com> diff --git a/cgit.c b/cgit.c index 22b6d7c..57e11cd 100644 --- a/cgit.c +++ b/cgit.c @@ -12,6 +12,7 @@ #include "configfile.h" #include "html.h" #include "ui-shared.h" +#include "ui-stats.h" #include "scan-tree.h" const char *cgit_version = CGIT_VERSION; @@ -54,8 +55,8 @@ void config_cb(const char *name, const char *value) ctx.cfg.enable_log_filecount = atoi(value); else if (!strcmp(name, "enable-log-linecount")) ctx.cfg.enable_log_linecount = atoi(value); - else if (!strcmp(name, "enable-stats")) - ctx.cfg.enable_stats = atoi(value); + else if (!strcmp(name, "max-stats")) + ctx.cfg.max_stats = cgit_find_stats_period(value, NULL); else if (!strcmp(name, "cache-size")) ctx.cfg.cache_size = atoi(value); else if (!strcmp(name, "cache-root")) @@ -114,8 +115,8 @@ void config_cb(const char *name, const char *value) ctx.repo->enable_log_filecount = ctx.cfg.enable_log_filecount * atoi(value); else if (ctx.repo && !strcmp(name, "repo.enable-log-linecount")) ctx.repo->enable_log_linecount = ctx.cfg.enable_log_linecount * atoi(value); - else if (ctx.repo && !strcmp(name, "repo.enable-stats")) - ctx.repo->enable_stats = ctx.cfg.enable_stats && atoi(value); + else if (ctx.repo && !strcmp(name, "repo.max-stats")) + ctx.repo->max_stats = cgit_find_stats_period(value, NULL); else if (ctx.repo && !strcmp(name, "repo.module-link")) ctx.repo->module_link= xstrdup(value); else if (ctx.repo && !strcmp(name, "repo.readme") && value != NULL) { @@ -183,6 +184,7 @@ static void prepare_context(struct cgit_context *ctx) ctx->cfg.max_lock_attempts = 5; ctx->cfg.max_msg_len = 80; ctx->cfg.max_repodesc_len = 80; + ctx->cfg.max_stats = 0; ctx->cfg.module_link = "./?repo=%s&page=commit&id=%s"; ctx->cfg.renamelimit = -1; ctx->cfg.robots = "index, nofollow"; diff --git a/cgit.h b/cgit.h index 85045c4..f2cb671 100644 --- a/cgit.h +++ b/cgit.h @@ -61,7 +61,7 @@ struct cgit_repo { int snapshots; int enable_log_filecount; int enable_log_linecount; - int enable_stats; + int max_stats; }; struct cgit_repolist { @@ -153,13 +153,13 @@ struct cgit_config { int enable_index_links; int enable_log_filecount; int enable_log_linecount; - int enable_stats; int local_time; int max_repo_count; int max_commit_count; int max_lock_attempts; int max_msg_len; int max_repodesc_len; + int max_stats; int nocache; int renamelimit; int snapshots; diff --git a/cgitrc.5.txt b/cgitrc.5.txt index 60d3ea4..0bbbea3 100644 --- a/cgitrc.5.txt +++ b/cgitrc.5.txt @@ -74,10 +74,6 @@ enable-log-linecount and removed lines for each commit on the repository log page. Default value: "0". -enable-stats - Globally enable/disable statistics for each repository. Default - value: "0". - favicon Url used as link to a shortcut icon for cgit. If specified, it is suggested to use the value "/favicon.ico" since certain browsers will @@ -133,6 +129,11 @@ max-repodesc-length Specifies the maximum number of repo description characters to display on the repository index page. Default value: "80". +max-stats + Set the default maximum statistics period. Valid values are "week", + "month", "quarter" and "year". If unspecified, statistics are + disabled. Default value: none. See also: "repo.max-stats". + module-link Text which will be used as the formatstring for a hyperlink when a submodule is printed in a directory listing. The arguments for the @@ -222,9 +223,10 @@ repo.enable-log-linecount A flag which can be used to disable the global setting `enable-log-linecount'. Default value: none. -repo.enable-stats - A flag which can be used to disable the global setting - `enable-stats'. Default value: none. +repo.max-stats + Override the default maximum statistics period. Valid values are equal + to the values specified for the global "max-stats" setting. Default + value: none. repo.name The value to show as repository name. Default value: <repo.url>. @@ -284,6 +286,10 @@ favicon=/favicon.ico logo=/img/mylogo.png +# Enable statistics per week, month and quarter +max-stats=quarter + + # Set the title and heading of the repository index page root-title=foobar.com git repositories @@ -356,6 +362,9 @@ repo.snapshots=0 # Disable line-counts for this repo repo.enable-log-linecount=0 +# Restrict the max statistics period for this repo +repo.max-stats=month + BUGS ---- diff --git a/cmd.c b/cmd.c index 744bf84..763a558 100644 --- a/cmd.c +++ b/cmd.c @@ -112,10 +112,7 @@ static void snapshot_fn(struct cgit_context *ctx) static void stats_fn(struct cgit_context *ctx) { - if (ctx->repo->enable_stats) - cgit_show_stats(ctx); - else - cgit_print_error("Stats disabled for this repo"); + cgit_show_stats(ctx); } static void summary_fn(struct cgit_context *ctx) diff --git a/shared.c b/shared.c index 37333f0..7382609 100644 --- a/shared.c +++ b/shared.c @@ -58,7 +58,7 @@ struct cgit_repo *cgit_add_repo(const char *url) ret->snapshots = ctx.cfg.snapshots; ret->enable_log_filecount = ctx.cfg.enable_log_filecount; ret->enable_log_linecount = ctx.cfg.enable_log_linecount; - ret->enable_stats = ctx.cfg.enable_stats; + ret->max_stats = ctx.cfg.max_stats; ret->module_link = ctx.cfg.module_link; ret->readme = NULL; return ret; diff --git a/ui-shared.c b/ui-shared.c index 0e688a0..97b9d46 100644 --- a/ui-shared.c +++ b/ui-shared.c @@ -641,7 +641,7 @@ void cgit_print_pageheader(struct cgit_context *ctx) ctx->qry.head, ctx->qry.sha1); cgit_diff_link("diff", NULL, hc(cmd, "diff"), ctx->qry.head, ctx->qry.sha1, ctx->qry.sha2, NULL); - if (ctx->repo->enable_stats) + if (ctx->repo->max_stats) reporevlink("stats", "stats", NULL, hc(cmd, "stats"), ctx->qry.head, NULL, NULL); if (ctx->repo->readme) diff --git a/ui-stats.c b/ui-stats.c index 3cc8d70..1104485 100644 --- a/ui-stats.c +++ b/ui-stats.c @@ -1,26 +1,12 @@ +#include <string-list.h> + #include "cgit.h" #include "html.h" -#include <string-list.h> +#include "ui-shared.h" +#include "ui-stats.h" #define MONTHS 6 -struct Period { - const char code; - const char *name; - int max_periods; - int count; - - /* Convert a tm value to the first day in the period */ - void (*trunc)(struct tm *tm); - - /* Update tm value to start of next/previous period */ - void (*dec)(struct tm *tm); - void (*inc)(struct tm *tm); - - /* Pretty-print a tm value */ - char *(*pretty)(struct tm *tm); -}; - struct authorstat { long total; struct string_list list; @@ -137,15 +123,39 @@ static char *pretty_year(struct tm *tm) return fmt("%d", tm->tm_year + 1900); } -struct Period periods[] = { +struct cgit_period periods[] = { {'w', "week", 12, 4, trunc_week, dec_week, inc_week, pretty_week}, {'m', "month", 12, 4, trunc_month, dec_month, inc_month, pretty_month}, {'q', "quarter", 12, 4, trunc_quarter, dec_quarter, inc_quarter, pretty_quarter}, {'y', "year", 12, 4, trunc_year, dec_year, inc_year, pretty_year}, }; +/* Given a period code or name, return a period index (1, 2, 3 or 4) + * and update the period pointer to the correcsponding struct. + * If no matching code is found, return 0. + */ +int cgit_find_stats_period(const char *expr, struct cgit_period **period) +{ + int i; + char code = '\0'; + + if (!expr) + return 0; + + if (strlen(expr) == 1) + code = expr[0]; + + for (i = 0; i < sizeof(periods) / sizeof(periods[0]); i++) + if (periods[i].code == code || !strcmp(periods[i].name, expr)) { + if (period) + *period = &periods[i]; + return i+1; + } + return 0; +} + static void add_commit(struct string_list *authors, struct commit *commit, - struct Period *period) + struct cgit_period *period) { struct commitinfo *info; struct string_list_item *author, *item; @@ -190,7 +200,7 @@ static int cmp_total_commits(const void *a1, const void *a2) * timeperiod into a nested string_list collection. */ struct string_list collect_stats(struct cgit_context *ctx, - struct Period *period) + struct cgit_period *period) { struct string_list authors; struct rev_info rev; @@ -233,7 +243,7 @@ struct string_list collect_stats(struct cgit_context *ctx, void print_combined_authorrow(struct string_list *authors, int from, int to, const char *name, const char *leftclass, const char *centerclass, - const char *rightclass, struct Period *period) + const char *rightclass, struct cgit_period *period) { struct string_list_item *author; struct authorstat *authorstat; @@ -271,7 +281,8 @@ void print_combined_authorrow(struct string_list *authors, int from, int to, htmlf("<td class='%s'>%d</td></tr>", rightclass, total); } -void print_authors(struct string_list *authors, int top, struct Period *period) +void print_authors(struct string_list *authors, int top, + struct cgit_period *period) { struct string_list_item *author; struct authorstat *authorstat; @@ -339,16 +350,22 @@ void print_authors(struct string_list *authors, int top, struct Period *period) void cgit_show_stats(struct cgit_context *ctx) { struct string_list authors; - struct Period *period; + struct cgit_period *period; int top, i; + const char *code = "w"; - period = &periods[0]; - if (ctx->qry.period) { - for (i = 0; i < sizeof(periods) / sizeof(periods[0]); i++) - if (periods[i].code == ctx->qry.period[0]) { - period = &periods[i]; - break; - } + if (ctx->qry.period) + code = ctx->qry.period; + + i = cgit_find_stats_period(code, &period); + if (!i) { + cgit_print_error(fmt("Unknown statistics type: %c", code)); + return; + } + if (i > ctx->repo->max_stats) { + cgit_print_error(fmt("Statistics type disabled: %s", + period->name)); + return; } authors = collect_stats(ctx, period); qsort(authors.items, authors.nr, sizeof(struct string_list_item), @@ -368,14 +385,16 @@ void cgit_show_stats(struct cgit_context *ctx) html("<form method='get' action='.' style='float: right; text-align: right;'>"); if (strcmp(ctx->qry.head, ctx->repo->defbranch)) htmlf("<input type='hidden' name='h' value='%s'/>", ctx->qry.head); - html("Period: "); - html("<select name='period' onchange='this.form.submit();'>"); - for (i = 0; i < sizeof(periods) / sizeof(periods[0]); i++) - htmlf("<option value='%c'%s>%s</option>", - periods[i].code, - period == &periods[i] ? " selected" : "", - periods[i].name); - html("</select><br/><br/>"); + if (ctx->repo->max_stats > 1) { + html("Period: "); + html("<select name='period' onchange='this.form.submit();'>"); + for (i = 0; i < ctx->repo->max_stats; i++) + htmlf("<option value='%c'%s>%s</option>", + periods[i].code, + period == &periods[i] ? " selected" : "", + periods[i].name); + html("</select><br/><br/>"); + } html("Authors: "); html(""); html("<select name='ofs' onchange='this.form.submit();'>"); diff --git a/ui-stats.h b/ui-stats.h index f1d744c..4f13dba 100644 --- a/ui-stats.h +++ b/ui-stats.h @@ -3,6 +3,25 @@ #include "cgit.h" +struct cgit_period { + const char code; + const char *name; + int max_periods; + int count; + + /* Convert a tm value to the first day in the period */ + void (*trunc)(struct tm *tm); + + /* Update tm value to start of next/previous period */ + void (*dec)(struct tm *tm); + void (*inc)(struct tm *tm); + + /* Pretty-print a tm value */ + char *(*pretty)(struct tm *tm); +}; + +extern int cgit_find_stats_period(const char *expr, struct cgit_period **period); + extern void cgit_show_stats(struct cgit_context *ctx); #endif /* UI_STATS_H */ -- cgit v0.10.1 From eaf2d25c162534efe7566119cf4b1cbeab217b51 Mon Sep 17 00:00:00 2001 From: Lars Hjemli <hjemli@gmail.com> Date: Sun, 7 Dec 2008 13:34:16 +0100 Subject: ui-shared: add and use cgit_stats_link() Signed-off-by: Lars Hjemli <hjemli@gmail.com> diff --git a/ui-shared.c b/ui-shared.c index 97b9d46..a5eb25f 100644 --- a/ui-shared.c +++ b/ui-shared.c @@ -363,6 +363,12 @@ void cgit_patch_link(char *name, char *title, char *class, char *head, reporevlink("patch", name, title, class, head, rev, NULL); } +void cgit_stats_link(char *name, char *title, char *class, char *head, + char *path) +{ + reporevlink("stats", name, title, class, head, NULL, path); +} + void cgit_object_link(struct object *obj) { char *page, *rev, *name; @@ -642,8 +648,8 @@ void cgit_print_pageheader(struct cgit_context *ctx) cgit_diff_link("diff", NULL, hc(cmd, "diff"), ctx->qry.head, ctx->qry.sha1, ctx->qry.sha2, NULL); if (ctx->repo->max_stats) - reporevlink("stats", "stats", NULL, hc(cmd, "stats"), - ctx->qry.head, NULL, NULL); + cgit_stats_link("stats", NULL, hc(cmd, "stats"), + ctx->qry.head, NULL); if (ctx->repo->readme) reporevlink("about", "about", NULL, hc(cmd, "about"), ctx->qry.head, NULL, diff --git a/ui-shared.h b/ui-shared.h index 3c8a6d0..e3fae64 100644 --- a/ui-shared.h +++ b/ui-shared.h @@ -30,6 +30,8 @@ extern void cgit_snapshot_link(char *name, char *title, char *class, char *head, char *rev, char *archivename); extern void cgit_diff_link(char *name, char *title, char *class, char *head, char *new_rev, char *old_rev, char *path); +extern void cgit_stats_link(char *name, char *title, char *class, char *head, + char *path); extern void cgit_object_link(struct object *obj); extern void cgit_print_error(char *msg); -- cgit v0.10.1 From 837d464af77d76a2661d92e05167657cbf430d80 Mon Sep 17 00:00:00 2001 From: Lars Hjemli <hjemli@gmail.com> Date: Sun, 7 Dec 2008 13:34:42 +0100 Subject: ui-tree: add link to stats page per path Signed-off-by: Lars Hjemli <hjemli@gmail.com> diff --git a/ui-tree.c b/ui-tree.c index 79332fc..e27e796 100644 --- a/ui-tree.c +++ b/ui-tree.c @@ -107,6 +107,9 @@ static int ls_item(const unsigned char *sha1, const char *base, int baselen, html("<td>"); cgit_log_link("log", NULL, "button", ctx.qry.head, curr_rev, fullpath, 0, NULL, NULL); + if (ctx.repo->max_stats) + cgit_stats_link("stats", NULL, "button", ctx.qry.head, + fullpath); html("</td></tr>\n"); free(name); return 0; -- cgit v0.10.1 From c3c925f4eca3e863bedaae4cfe237c3ad52092b6 Mon Sep 17 00:00:00 2001 From: Lars Hjemli <hjemli@gmail.com> Date: Sun, 7 Dec 2008 15:52:35 +0100 Subject: ui-shared: externalize add_hidden_formfields() This function will be reused by ui-stats.c in the next commit. Signed-off-by: Lars Hjemli <hjemli@gmail.com> diff --git a/ui-shared.c b/ui-shared.c index a5eb25f..1bb30c2 100644 --- a/ui-shared.c +++ b/ui-shared.c @@ -555,7 +555,7 @@ int print_archive_ref(const char *refname, const unsigned char *sha1, return 0; } -void add_hidden_formfields(int incl_head, int incl_search, char *page) +void cgit_add_hidden_formfields(int incl_head, int incl_search, char *page) { char *url; @@ -610,7 +610,7 @@ void cgit_print_pageheader(struct cgit_context *ctx) cgit_summary_link(ctx->repo->name, ctx->repo->name, NULL, NULL); html("</td><td class='form'>"); html("<form method='get' action=''>\n"); - add_hidden_formfields(0, 1, ctx->qry.page); + cgit_add_hidden_formfields(0, 1, ctx->qry.page); html("<select name='h' onchange='this.form.submit();'>\n"); for_each_branch_ref(print_branch_option, ctx->qry.head); html("</select> "); @@ -660,7 +660,7 @@ void cgit_print_pageheader(struct cgit_context *ctx) html_url_path(cgit_fileurl(ctx->qry.repo, "log", ctx->qry.path, NULL)); html("'>\n"); - add_hidden_formfields(1, 0, "log"); + 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); diff --git a/ui-shared.h b/ui-shared.h index e3fae64..1524dc3 100644 --- a/ui-shared.h +++ b/ui-shared.h @@ -44,5 +44,6 @@ extern void cgit_print_pageheader(struct cgit_context *ctx); extern void cgit_print_filemode(unsigned short mode); extern void cgit_print_snapshot_links(const char *repo, const char *head, const char *hex, int snapshots); - +extern void cgit_add_hidden_formfields(int incl_head, int incl_search, + char *page); #endif /* UI_SHARED_H */ -- cgit v0.10.1 From b54ef9749c083afd86573112fad3b3ed8ee2d0e4 Mon Sep 17 00:00:00 2001 From: Lars Hjemli <hjemli@gmail.com> Date: Sun, 7 Dec 2008 15:57:55 +0100 Subject: ui-stats.c: reuse cgit_add_hidden_formfields() This makes sure that the currect branch and path is preserved when changing the stats period or author count. Signed-off-by: Lars Hjemli <hjemli@gmail.com> diff --git a/ui-stats.c b/ui-stats.c index 1104485..9fc06d3 100644 --- a/ui-stats.c +++ b/ui-stats.c @@ -382,9 +382,8 @@ void cgit_show_stats(struct cgit_context *ctx) } html("</h2>"); - html("<form method='get' action='.' style='float: right; text-align: right;'>"); - if (strcmp(ctx->qry.head, ctx->repo->defbranch)) - htmlf("<input type='hidden' name='h' value='%s'/>", ctx->qry.head); + html("<form method='get' action='' style='float: right; text-align: right;'>"); + cgit_add_hidden_formfields(1, 0, "stats"); if (ctx->repo->max_stats > 1) { html("Period: "); html("<select name='period' onchange='this.form.submit();'>"); -- cgit v0.10.1